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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +80 -6
  2. package/dist/comments/comment-body.js +2 -2
  3. package/dist/comments/comment-directives.js +4 -0
  4. package/dist/comments/comment-eligibility.d.ts +1 -0
  5. package/dist/comments/comment-eligibility.js +4 -1
  6. package/dist/comments/comment-location.js +24 -6
  7. package/dist/comments/embedded-expression-ranges.d.ts +2 -3
  8. package/dist/comments/embedded-expression-ranges.js +39 -27
  9. package/dist/comments/jsx-expression-layout.d.ts +2 -2
  10. package/dist/comments/jsx-expression-layout.js +16 -19
  11. package/dist/comments/prettier-ignore.d.ts +2 -1
  12. package/dist/comments/prettier-ignore.js +110 -16
  13. package/dist/comments/printer-layout.d.ts +1 -0
  14. package/dist/comments/printer-layout.js +1 -0
  15. package/dist/comments/wrap-block-comment.d.ts +1 -0
  16. package/dist/comments/wrap-block-comment.js +3 -1
  17. package/dist/comments/wrap-comments.js +75 -7
  18. package/dist/comments/wrap-trailing-line-comment.d.ts +1 -1
  19. package/dist/index.d.ts +6 -2
  20. package/dist/index.js +4 -1
  21. package/dist/plugin/create-parsers.js +92 -5
  22. package/dist/plugin/create-printers.js +23 -2
  23. package/dist/plugin/get-printer-layout-source.js +10 -2
  24. package/dist/plugin/plugin-name.d.ts +1 -0
  25. package/dist/plugin/plugin-name.js +1 -0
  26. package/dist/utils/ast.d.ts +6 -0
  27. package/dist/utils/ast.js +43 -0
  28. package/dist/utils/display-width.d.ts +1 -1
  29. package/dist/utils/display-width.js +3 -3
  30. package/dist/utils/format-markdown.js +50 -11
  31. package/dist/utils/source-lines.d.ts +1 -0
  32. package/dist/utils/source-lines.js +3 -0
  33. package/dist/utils/whitespace.d.ts +1 -0
  34. package/dist/utils/whitespace.js +4 -0
  35. package/package.json +15 -6
@@ -37,7 +37,9 @@ function buildBlockReplacement(text, comment, formattedLines, options, layout) {
37
37
  const singleLine = `/* ${formattedLines.join(' ')} */`;
38
38
  const singleLineWidth = getColumns(singleLine, tabWidth);
39
39
  const singleLineSuffixWidth = layout.singleLineSuffixWidth ?? 0;
40
- if (formattedLines.length === 1 && markerColumn + singleLineWidth + singleLineSuffixWidth <= getPrintWidth(options)) {
40
+ if (layout.preserveMultiline !== true &&
41
+ formattedLines.length === 1 &&
42
+ markerColumn + singleLineWidth + singleLineSuffixWidth <= getPrintWidth(options)) {
41
43
  return singleLine;
42
44
  }
43
45
  if (layout.placement === 'inline') {
@@ -1,7 +1,7 @@
1
1
  import { shouldSkipLineComment } from './comment-eligibility.js';
2
2
  import { isStandaloneBlockComment, isStandaloneLineComment } from './comment-location.js';
3
3
  import { collectCommentEntries } from './comment-ranges.js';
4
- import { collectEmbeddedExpressionRanges, doesBlockCommentSeparateEmbeddedTrailingLineComment, getEmbeddedTrailingLineCommentMove, isCommentInEmbeddedExpression, } from './embedded-expression-ranges.js';
4
+ import { collectEmbeddedExpressionRanges, doesBlockCommentSeparateEmbeddedTrailingLineComment, getEmbeddedTrailingLineCommentMove, } from './embedded-expression-ranges.js';
5
5
  import { collectJsxExpressionContainerRanges, getJsxExpressionBlockCommentLayout } from './jsx-expression-layout.js';
6
6
  import { collectLineCommentGroup } from './line-comment-groups.js';
7
7
  import { collectPrettierIgnoredLineRanges, isCommentInIgnoredLineRange, isPrettierIgnoredBlockComment, isPrettierIgnoredStandaloneLineComment, isPrettierIgnoredTrailingLineComment, } from './prettier-ignore.js';
@@ -9,7 +9,10 @@ import { getPrinterLayout } from './printer-layout.js';
9
9
  import { wrapBlockComment } from './wrap-block-comment.js';
10
10
  import { wrapLineCommentGroup } from './wrap-line-comment-group.js';
11
11
  import { wrapTrailingLineComment } from './wrap-trailing-line-comment.js';
12
+ import { matchOrderedRangesToSmallestContainers } from '../utils/ast.js';
13
+ import { getColumnAt, getColumns } from '../utils/display-width.js';
12
14
  import { applyReplacements } from '../utils/replacements.js';
15
+ import { getLineEnd, getLineStart } from '../utils/source-lines.js';
13
16
  import { getTabWidth } from '../utils/wrap-options.js';
14
17
  export async function wrapComments(text, ast, options, printerLayoutSource) {
15
18
  return (await wrapCommentsWithMetadata(text, ast, options, printerLayoutSource)).text;
@@ -20,6 +23,8 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
20
23
  const embeddedExpressionRanges = collectEmbeddedExpressionRanges(ast);
21
24
  const jsxExpressionContainers = collectJsxExpressionContainerRanges(ast);
22
25
  const ignoredLineRanges = collectPrettierIgnoredLineRanges(text, ast, commentEntries);
26
+ const embeddedExpressionRangeMatches = matchOrderedRangesToSmallestContainers(comments, embeddedExpressionRanges);
27
+ const jsxExpressionContainerMatches = matchOrderedRangesToSmallestContainers(comments, jsxExpressionContainers);
23
28
  if (comments.length === 0) {
24
29
  return { jsxBlockCommentRewrites: [], text };
25
30
  }
@@ -27,21 +32,34 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
27
32
  const jsxBlockCommentRewrites = [];
28
33
  const tabWidth = getTabWidth(options);
29
34
  const printerLayout = getPrinterLayout(text, commentEntries, jsxExpressionContainers, printerLayoutSource, tabWidth);
35
+ const sameLineWidthDeltas = {
36
+ output: new Map(),
37
+ source: new Map(),
38
+ };
30
39
  let blockCommentIndex = -1;
40
+ let ignoredLineRangeIndex = 0;
31
41
  for (let index = 0; index < comments.length; index += 1) {
32
42
  const comment = comments[index];
33
43
  const outputCommentLayout = printerLayout.comments[index];
34
44
  if (comment?.kind === 'block') {
35
45
  blockCommentIndex += 1;
36
46
  }
37
- if (comment === undefined || isCommentInIgnoredLineRange(comment, ignoredLineRanges)) {
47
+ if (comment === undefined) {
48
+ continue;
49
+ }
50
+ let ignoredLineRange = ignoredLineRanges[ignoredLineRangeIndex];
51
+ while (ignoredLineRange !== undefined && ignoredLineRange.end <= comment.start) {
52
+ ignoredLineRangeIndex += 1;
53
+ ignoredLineRange = ignoredLineRanges[ignoredLineRangeIndex];
54
+ }
55
+ if (isCommentInIgnoredLineRange(comment, ignoredLineRange)) {
38
56
  continue;
39
57
  }
40
58
  if (comment.kind === 'block') {
41
59
  if (isPrettierIgnoredBlockComment(text, commentEntries, index)) {
42
60
  continue;
43
61
  }
44
- const jsxLayout = getJsxExpressionBlockCommentLayout(text, comment, comments[index - 1], jsxExpressionContainers, tabWidth, outputCommentLayout, printerLayout.jsxCommentMarkerColumns);
62
+ const jsxLayout = getJsxExpressionBlockCommentLayout(text, comment, comments[index - 1], jsxExpressionContainerMatches[index], tabWidth, outputCommentLayout, printerLayout.jsxCommentMarkerColumns);
45
63
  if (jsxLayout?.placement === 'inline') {
46
64
  continue;
47
65
  }
@@ -52,7 +70,7 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
52
70
  placement: isStandaloneBlockComment(text, comment) ? 'standalone' : 'inline',
53
71
  };
54
72
  const replacement = await wrapBlockComment(text, comment, options, jsxLayout ?? outputLayout);
55
- const preservesEmbeddedCommentOrder = doesBlockCommentSeparateEmbeddedTrailingLineComment(text, comment, comments[index + 1], embeddedExpressionRanges);
73
+ const preservesEmbeddedCommentOrder = doesBlockCommentSeparateEmbeddedTrailingLineComment(text, comment, comments[index + 1], embeddedExpressionRangeMatches[index]?.range, embeddedExpressionRangeMatches[index + 1]?.range);
56
74
  if (Array.isArray(replacement)) {
57
75
  if (!preservesEmbeddedCommentOrder) {
58
76
  replacements.push(...replacement);
@@ -60,6 +78,7 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
60
78
  }
61
79
  else if (replacement !== undefined) {
62
80
  replacements.push(replacement);
81
+ recordSameLineReplacementWidthDelta(text, comment, replacement, outputCommentLayout, tabWidth, sameLineWidthDeltas);
63
82
  if (jsxLayout !== undefined) {
64
83
  jsxBlockCommentRewrites.push({ blockCommentIndex, text: replacement.text });
65
84
  }
@@ -77,11 +96,13 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
77
96
  if (isPrettierIgnoredTrailingLineComment(text, commentEntries, index)) {
78
97
  continue;
79
98
  }
80
- const embeddedMove = getEmbeddedTrailingLineCommentMove(text, comment, embeddedExpressionRanges);
81
- if (embeddedMove === undefined && isCommentInEmbeddedExpression(comment, embeddedExpressionRanges)) {
99
+ const embeddedExpressionRange = embeddedExpressionRangeMatches[index]?.range;
100
+ const embeddedMove = getEmbeddedTrailingLineCommentMove(text, comment, embeddedExpressionRange);
101
+ if (embeddedMove === undefined && embeddedExpressionRange !== undefined) {
82
102
  continue;
83
103
  }
84
- const replacement = await wrapTrailingLineComment(text, comment, options, outputCommentLayout, embeddedMove);
104
+ const trailingLayout = getTrailingLineCommentLayout(text, comment, outputCommentLayout, tabWidth, sameLineWidthDeltas);
105
+ const replacement = await wrapTrailingLineComment(text, comment, options, trailingLayout, embeddedMove);
85
106
  if (replacement !== undefined) {
86
107
  replacements.push(...replacement);
87
108
  }
@@ -99,3 +120,50 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
99
120
  text: applyReplacements(text, replacements),
100
121
  };
101
122
  }
123
+ function recordSameLineReplacementWidthDelta(text, comment, replacement, outputLayout, tabWidth, sameLineWidthDeltas) {
124
+ const original = text.slice(comment.start, comment.end);
125
+ if (containsLineTerminator(original) || containsLineTerminator(replacement.text)) {
126
+ return;
127
+ }
128
+ const sourceLineStart = getLineStart(text, comment.start);
129
+ const sourceWidthDelta = sameLineWidthDeltas.source.get(sourceLineStart) ?? 0;
130
+ const sourceMarkerColumn = getColumnAt(text, comment.start, tabWidth) + sourceWidthDelta;
131
+ const sourceReplacementWidthDelta = getReplacementWidthDelta(original, replacement.text, sourceMarkerColumn, tabWidth);
132
+ addLineWidthDelta(sameLineWidthDeltas.source, sourceLineStart, sourceReplacementWidthDelta);
133
+ if (outputLayout === undefined) {
134
+ return;
135
+ }
136
+ const outputWidthDelta = sameLineWidthDeltas.output.get(outputLayout.lineStart) ?? 0;
137
+ const outputMarkerColumn = outputLayout.markerColumn + outputWidthDelta;
138
+ const outputReplacementWidthDelta = getReplacementWidthDelta(original, replacement.text, outputMarkerColumn, tabWidth);
139
+ addLineWidthDelta(sameLineWidthDeltas.output, outputLayout.lineStart, outputReplacementWidthDelta);
140
+ }
141
+ function getTrailingLineCommentLayout(text, comment, outputLayout, tabWidth, sameLineWidthDeltas) {
142
+ if (outputLayout !== undefined) {
143
+ const lineWidthDelta = sameLineWidthDeltas.output.get(outputLayout.lineStart) ?? 0;
144
+ return lineWidthDelta === 0
145
+ ? outputLayout
146
+ : { ...outputLayout, lineWidth: outputLayout.lineWidth + lineWidthDelta };
147
+ }
148
+ const lineStart = getLineStart(text, comment.start);
149
+ const lineWidthDelta = sameLineWidthDeltas.source.get(lineStart) ?? 0;
150
+ if (lineWidthDelta === 0) {
151
+ return undefined;
152
+ }
153
+ const lineEnd = getLineEnd(text, comment.start);
154
+ const lineText = text.slice(lineStart, lineEnd).replace(/[ \t]+$/u, '');
155
+ return { lineWidth: getColumns(lineText, tabWidth) + lineWidthDelta };
156
+ }
157
+ function getReplacementWidthDelta(original, replacement, markerColumn, tabWidth) {
158
+ const originalWidth = getColumns(original, tabWidth, markerColumn);
159
+ const replacementWidth = getColumns(replacement, tabWidth, markerColumn);
160
+ return replacementWidth - originalWidth;
161
+ }
162
+ function addLineWidthDelta(lineWidthDeltas, lineStart, delta) {
163
+ if (delta !== 0) {
164
+ lineWidthDeltas.set(lineStart, (lineWidthDeltas.get(lineStart) ?? 0) + delta);
165
+ }
166
+ }
167
+ function containsLineTerminator(text) {
168
+ return /[\r\n\u2028\u2029]/u.test(text);
169
+ }
@@ -3,7 +3,7 @@ import type { EmbeddedTrailingLineCommentMove } from './embedded-expression-rang
3
3
  import type { Replacement } from '../utils/replacements.js';
4
4
  import type { WrapOptions } from '../utils/wrap-options.js';
5
5
  export type TrailingLineCommentLayout = {
6
- lineIndentColumn: number;
6
+ lineIndentColumn?: number;
7
7
  lineWidth: number;
8
8
  };
9
9
  export declare function wrapTrailingLineComment(text: string, comment: CommentRange, options: WrapOptions, outputLayout?: TrailingLineCommentLayout, move?: EmbeddedTrailingLineCommentMove): Promise<Replacement[] | undefined>;
package/dist/index.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import type { Plugin } from 'prettier';
2
+ declare const name = "@aforemendude/prettier-plugin-wrap-comments";
2
3
  declare const parsers: {
3
4
  [parserName: string]: import("prettier").Parser<any>;
4
5
  };
5
6
  declare const printers: {
6
7
  [astFormat: string]: import("prettier").Printer<any>;
7
8
  };
8
- declare const plugin: Plugin;
9
- export { parsers, printers };
9
+ declare const plugin: NamedPlugin;
10
+ export { name, parsers, printers };
10
11
  export default plugin;
12
+ interface NamedPlugin extends Plugin {
13
+ name: string;
14
+ }
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { createParsers } from './plugin/create-parsers.js';
2
2
  import { createPrinters } from './plugin/create-printers.js';
3
+ import { PLUGIN_NAME } from './plugin/plugin-name.js';
4
+ const name = PLUGIN_NAME;
3
5
  const parsers = createParsers();
4
6
  const printers = createPrinters();
5
7
  const plugin = {
8
+ name,
6
9
  parsers,
7
10
  printers,
8
11
  };
9
- export { parsers, printers };
12
+ export { name, parsers, printers };
10
13
  export default plugin;
@@ -6,6 +6,7 @@ import { wrapCommentsWithMetadata } from '../comments/wrap-comments.js';
6
6
  import { getPrinterLayoutSource } from './get-printer-layout-source.js';
7
7
  import { markRewrittenJsxBlockComments, setJsxBlockCommentRewrites } from './jsx-comment-rewrite-metadata.js';
8
8
  import { SUPPORTED_PARSER_NAMES } from './parser-names.js';
9
+ import { PLUGIN_NAME } from './plugin-name.js';
9
10
  export function createParsers() {
10
11
  const parsers = {};
11
12
  for (const parserName of SUPPORTED_PARSER_NAMES) {
@@ -18,23 +19,28 @@ export function createParsers() {
18
19
  return parsers;
19
20
  }
20
21
  function createWrappedParser(parserName, parser) {
21
- return {
22
+ const wrappedParser = {
22
23
  ...parser,
23
24
  async parse(text, options) {
24
- const ast = await parser.parse(text, options);
25
+ const delegate = await resolveParserDelegate(parserName, parser, wrappedParser, options);
26
+ const ast = await parseWithDelegate(text, delegate, options);
25
27
  const neutralizedAst = neutralizePrettierIgnoreForIgnoredComments(text, ast);
26
28
  markRewrittenJsxBlockComments(text, neutralizedAst, options);
27
29
  return neutralizedAst;
28
30
  },
29
31
  async preprocess(text, options) {
30
32
  setJsxBlockCommentRewrites(options, []);
31
- const preprocessed = parser.preprocess === undefined ? text : await parser.preprocess(text, options);
33
+ const delegate = await resolveParserDelegate(parserName, parser, wrappedParser, options);
34
+ const preprocessed = await preprocessWithDelegate(text, delegate, options);
32
35
  if (hasOffsetSensitiveFormatting(text, options)) {
33
36
  return preprocessed;
34
37
  }
38
+ if (!preprocessed.includes('//') && !preprocessed.includes('/*')) {
39
+ return preprocessed;
40
+ }
35
41
  let ast;
36
42
  try {
37
- ast = await parser.parse(preprocessed, options);
43
+ ast = await parseWithDelegate(preprocessed, delegate, { ...options, parser: parserName });
38
44
  }
39
45
  catch {
40
46
  return preprocessed;
@@ -42,12 +48,93 @@ function createWrappedParser(parserName, parser) {
42
48
  if (collectAstComments(ast).length === 0) {
43
49
  return preprocessed;
44
50
  }
45
- const printerLayoutSource = await getPrinterLayoutSource(preprocessed, ast, parserName, parser, options);
51
+ const printerLayoutSource = await getPrinterLayoutSource(preprocessed, ast, parserName, delegate.parser, createDelegateOptions(options, delegate.plugins));
46
52
  const result = await wrapCommentsWithMetadata(preprocessed, ast, options, printerLayoutSource);
47
53
  setJsxBlockCommentRewrites(options, result.jsxBlockCommentRewrites);
48
54
  return result.text;
49
55
  },
50
56
  };
57
+ return wrappedParser;
58
+ }
59
+ async function resolveParserDelegate(parserName, nativeParser, wrappedParser, options) {
60
+ const plugins = options.plugins ?? [];
61
+ const wrappedPluginIndex = findWrappedPluginIndex(plugins, parserName, wrappedParser);
62
+ if (wrappedPluginIndex === -1) {
63
+ return { parser: nativeParser };
64
+ }
65
+ for (let index = wrappedPluginIndex - 1; index >= 0; index -= 1) {
66
+ const plugin = getPlugin(plugins[index]);
67
+ const parserEntry = getParserEntry(plugin, parserName);
68
+ if (parserEntry === undefined) {
69
+ continue;
70
+ }
71
+ const precedingParser = typeof parserEntry === 'function' ? await parserEntry() : parserEntry;
72
+ if (isParser(precedingParser)) {
73
+ return {
74
+ parser: precedingParser,
75
+ plugins: plugins.slice(0, wrappedPluginIndex),
76
+ };
77
+ }
78
+ }
79
+ return { parser: nativeParser };
80
+ }
81
+ async function parseWithDelegate(text, delegate, options) {
82
+ return withDelegatePlugins(options, delegate.plugins, async () => delegate.parser.parse(text, options));
83
+ }
84
+ async function preprocessWithDelegate(text, delegate, options) {
85
+ const preprocess = delegate.parser.preprocess;
86
+ if (preprocess === undefined) {
87
+ return text;
88
+ }
89
+ return withDelegatePlugins(options, delegate.plugins, async () => preprocess(text, options));
90
+ }
91
+ async function withDelegatePlugins(options, plugins, callback) {
92
+ if (plugins === undefined) {
93
+ return callback();
94
+ }
95
+ const originalPlugins = options.plugins;
96
+ options.plugins = plugins;
97
+ try {
98
+ return await callback();
99
+ }
100
+ finally {
101
+ options.plugins = originalPlugins;
102
+ }
103
+ }
104
+ function createDelegateOptions(options, plugins) {
105
+ return plugins === undefined ? options : { ...options, plugins };
106
+ }
107
+ function findWrappedPluginIndex(plugins, parserName, wrappedParser) {
108
+ for (let index = plugins.length - 1; index >= 0; index -= 1) {
109
+ const plugin = getPlugin(plugins[index]);
110
+ const parserEntry = getParserEntry(plugin, parserName);
111
+ if (parserEntry === wrappedParser) {
112
+ return index;
113
+ }
114
+ }
115
+ for (let index = plugins.length - 1; index >= 0; index -= 1) {
116
+ const plugin = getPlugin(plugins[index]);
117
+ const parserEntry = getParserEntry(plugin, parserName);
118
+ if (plugin?.name === PLUGIN_NAME && parserEntry !== undefined) {
119
+ return index;
120
+ }
121
+ }
122
+ return -1;
123
+ }
124
+ function getPlugin(plugin) {
125
+ if (typeof plugin !== 'object' || plugin === null || plugin instanceof URL) {
126
+ return undefined;
127
+ }
128
+ return plugin;
129
+ }
130
+ function getParserEntry(plugin, parserName) {
131
+ if (plugin?.parsers === undefined || !Object.hasOwn(plugin.parsers, parserName)) {
132
+ return undefined;
133
+ }
134
+ return plugin.parsers[parserName];
135
+ }
136
+ function isParser(value) {
137
+ return typeof value === 'object' && value !== null && 'parse' in value && typeof value.parse === 'function';
51
138
  }
52
139
  function hasOffsetSensitiveFormatting(text, options) {
53
140
  const cursorOffset = options['cursorOffset'];
@@ -1,12 +1,18 @@
1
1
  import { doc } from 'prettier';
2
2
  import * as estreePlugin from 'prettier/plugins/estree';
3
+ import { getNeutralizedPrettierIgnoreOriginalText } from '../comments/prettier-ignore.js';
4
+ import { normalizeLineTerminators } from '../utils/source-lines.js';
3
5
  import { isRecord } from '../utils/type-guards.js';
4
6
  import { isRewrittenJsxBlockComment } from './jsx-comment-rewrite-metadata.js';
5
7
  const { hardline, indent } = doc.builders;
8
+ const { mapDoc, replaceEndOfLine } = doc.utils;
6
9
  export function createPrinters() {
7
10
  const estreePrinter = estreePlugin.printers.estree;
11
+ const estreePrintComment = estreePrinter.printComment;
12
+ if (estreePrintComment === undefined) {
13
+ throw new Error('Expected the native estree printer to provide printComment');
14
+ }
8
15
  return {
9
- ...estreePlugin.printers,
10
16
  estree: {
11
17
  ...estreePrinter,
12
18
  print(path, options, print, args) {
@@ -15,6 +21,21 @@ export function createPrinters() {
15
21
  }
16
22
  return estreePrinter.print(path, options, print, args);
17
23
  },
24
+ printComment(path, options) {
25
+ const originalText = getNeutralizedPrettierIgnoreOriginalText(path.node);
26
+ if (originalText !== undefined) {
27
+ const normalizedOriginalText = normalizeLineTerminators(originalText);
28
+ return normalizedOriginalText.includes('\n')
29
+ ? replaceEndOfLine(normalizedOriginalText)
30
+ : normalizedOriginalText;
31
+ }
32
+ if (isRewrittenMultilineBlockComment(path.node)) {
33
+ const printedComment = estreePrintComment(path, options);
34
+ const normalizedComment = mapDoc(printedComment, (currentDoc) => typeof currentDoc === 'string' ? normalizeLineTerminators(currentDoc) : currentDoc);
35
+ return replaceEndOfLine(normalizedComment, hardline);
36
+ }
37
+ return estreePrintComment(path, options);
38
+ },
18
39
  },
19
40
  };
20
41
  }
@@ -34,5 +55,5 @@ function isRewrittenMultilineBlockComment(comment) {
34
55
  return false;
35
56
  }
36
57
  const value = comment['value'];
37
- return typeof value === 'string' && value.includes('\n') && isRewrittenJsxBlockComment(comment);
58
+ return typeof value === 'string' && /[\r\n\u2028\u2029]/u.test(value) && isRewrittenJsxBlockComment(comment);
38
59
  }
@@ -1,8 +1,16 @@
1
1
  import { format } from 'prettier';
2
+ import { neutralizePrettierIgnoreForIgnoredComments } from '../comments/prettier-ignore.js';
2
3
  import { createPrinters } from './create-printers.js';
3
4
  export async function getPrinterLayoutSource(text, ast, parserName, parser, options) {
5
+ const printerLayoutParser = {
6
+ ...parser,
7
+ async parse(source, parserOptions) {
8
+ const printerLayoutAst = await parser.parse(source, parserOptions);
9
+ return neutralizePrettierIgnoreForIgnoredComments(source, printerLayoutAst);
10
+ },
11
+ };
4
12
  const printerLayoutPlugin = {
5
- parsers: { [parserName]: parser },
13
+ parsers: { [parserName]: printerLayoutParser },
6
14
  printers: createPrinters(),
7
15
  };
8
16
  try {
@@ -15,7 +23,7 @@ export async function getPrinterLayoutSource(text, ast, parserName, parser, opti
15
23
  if (formattedText === text) {
16
24
  return { ast, text };
17
25
  }
18
- const formattedAst = await parser.parse(formattedText, options);
26
+ const formattedAst = await parser.parse(formattedText, { ...options, parser: parserName });
19
27
  return {
20
28
  ast: formattedAst,
21
29
  text: formattedText,
@@ -0,0 +1 @@
1
+ export declare const PLUGIN_NAME = "@aforemendude/prettier-plugin-wrap-comments";
@@ -0,0 +1 @@
1
+ export const PLUGIN_NAME = '@aforemendude/prettier-plugin-wrap-comments';
@@ -2,5 +2,11 @@ export type SourceRange = {
2
2
  end: number;
3
3
  start: number;
4
4
  };
5
+ export type ContainingRangeMatch<Range extends SourceRange> = {
6
+ index: number;
7
+ range: Range;
8
+ };
5
9
  export declare function visitAstNodes(ast: unknown, visitor: (node: Record<string, unknown>) => void): void;
6
10
  export declare function getAstNodeRange(node: Record<string, unknown>): SourceRange | undefined;
11
+ export declare function collectAstNodeRangesByStart(ast: unknown): Map<number, SourceRange>;
12
+ export declare function matchOrderedRangesToSmallestContainers<Range extends SourceRange>(targets: readonly SourceRange[], containers: readonly Range[]): Array<ContainingRangeMatch<Range> | undefined>;
package/dist/utils/ast.js CHANGED
@@ -44,6 +44,49 @@ export function getAstNodeRange(node) {
44
44
  }
45
45
  return { end, start };
46
46
  }
47
+ export function collectAstNodeRangesByStart(ast) {
48
+ const rangesByStart = new Map();
49
+ visitAstNodes(ast, (node) => {
50
+ const range = getAstNodeRange(node);
51
+ if (range === undefined) {
52
+ return;
53
+ }
54
+ const existingRange = rangesByStart.get(range.start);
55
+ if (existingRange === undefined || range.end > existingRange.end) {
56
+ rangesByStart.set(range.start, range);
57
+ }
58
+ });
59
+ return rangesByStart;
60
+ }
61
+ export function matchOrderedRangesToSmallestContainers(targets, containers) {
62
+ const matches = [];
63
+ const openContainers = [];
64
+ let containerIndex = 0;
65
+ for (const target of targets) {
66
+ let container = containers[containerIndex];
67
+ while (container !== undefined && container.start < target.start) {
68
+ let previousContainer = openContainers[openContainers.length - 1];
69
+ while (previousContainer !== undefined && previousContainer.range.end <= container.start) {
70
+ openContainers.pop();
71
+ previousContainer = openContainers[openContainers.length - 1];
72
+ }
73
+ if (previousContainer === undefined ||
74
+ previousContainer.range.start !== container.start ||
75
+ previousContainer.range.end !== container.end) {
76
+ openContainers.push({ index: containerIndex, range: container });
77
+ }
78
+ containerIndex += 1;
79
+ container = containers[containerIndex];
80
+ }
81
+ let containingRange = openContainers[openContainers.length - 1];
82
+ while (containingRange !== undefined && containingRange.range.end <= target.end) {
83
+ openContainers.pop();
84
+ containingRange = openContainers[openContainers.length - 1];
85
+ }
86
+ matches.push(containingRange);
87
+ }
88
+ return matches;
89
+ }
47
90
  function getRangeNumber(range, index) {
48
91
  if (!Array.isArray(range)) {
49
92
  return undefined;
@@ -1,2 +1,2 @@
1
1
  export declare function getColumnAt(text: string, index: number, tabWidth: number): number;
2
- export declare function getColumns(text: string, tabWidth: number): number;
2
+ export declare function getColumns(text: string, tabWidth: number, startColumn?: number): number;
@@ -3,8 +3,8 @@ import { getLineStart } from './source-lines.js';
3
3
  export function getColumnAt(text, index, tabWidth) {
4
4
  return getColumns(text.slice(getLineStart(text, index), index), tabWidth);
5
5
  }
6
- export function getColumns(text, tabWidth) {
7
- let column = 0;
6
+ export function getColumns(text, tabWidth, startColumn = 0) {
7
+ let column = startColumn;
8
8
  let segmentStart = 0;
9
9
  for (let index = 0; index < text.length; index += 1) {
10
10
  if (text[index] === '\t') {
@@ -15,5 +15,5 @@ export function getColumns(text, tabWidth) {
15
15
  segmentStart = index + 1;
16
16
  }
17
17
  }
18
- return column + util.getStringWidth(text.slice(segmentStart));
18
+ return column + util.getStringWidth(text.slice(segmentStart)) - startColumn;
19
19
  }
@@ -1,22 +1,61 @@
1
1
  import { format } from 'prettier';
2
- import { isBlankLine } from './source-lines.js';
2
+ import { isBlankLine, normalizeLineTerminators } from './source-lines.js';
3
3
  import { getTabWidth } from './wrap-options.js';
4
+ const MAX_MARKDOWN_FORMAT_PASSES = 3;
4
5
  export async function formatMarkdownLines(markdown, printWidth, options) {
5
- const normalized = trimBlankEdges(markdown.replace(/\r\n?/g, '\n'));
6
+ const normalized = trimBlankEdges(normalizeLineTerminators(markdown));
7
+ let formatted;
6
8
  try {
7
- const formatted = await format(normalized, {
8
- endOfLine: 'lf',
9
- parser: 'markdown',
10
- printWidth,
11
- proseWrap: 'always',
12
- tabWidth: getTabWidth(options),
13
- useTabs: options.useTabs,
14
- });
15
- return formatted.replace(/\n$/, '').split('\n');
9
+ formatted = await formatMarkdown(normalized, printWidth, options);
16
10
  }
17
11
  catch {
18
12
  return normalized.split('\n');
19
13
  }
14
+ if (startsWithThematicRule(normalized)) {
15
+ formatted = await stabilizePotentialFrontMatter(formatted, printWidth, options);
16
+ }
17
+ return formatted.replace(/\n$/, '').split('\n');
18
+ }
19
+ async function formatMarkdown(markdown, printWidth, options) {
20
+ return format(markdown, {
21
+ endOfLine: 'lf',
22
+ parser: 'markdown',
23
+ printWidth,
24
+ proseWrap: 'always',
25
+ tabWidth: getTabWidth(options),
26
+ useTabs: options.useTabs,
27
+ });
28
+ }
29
+ async function stabilizePotentialFrontMatter(formatted, printWidth, options) {
30
+ const seen = new Set([formatted]);
31
+ let current = formatted;
32
+ for (let pass = 1; pass < MAX_MARKDOWN_FORMAT_PASSES && hasPotentialFrontMatterAmbiguity(current); pass += 1) {
33
+ let next;
34
+ try {
35
+ next = await formatMarkdown(current, printWidth, options);
36
+ }
37
+ catch {
38
+ return current;
39
+ }
40
+ if (next === current || seen.has(next)) {
41
+ return current;
42
+ }
43
+ seen.add(next);
44
+ current = next;
45
+ }
46
+ return current;
47
+ }
48
+ function startsWithThematicRule(markdown) {
49
+ const [firstLine = ''] = markdown.split('\n', 1);
50
+ return /^[ \t]{0,3}([*_-])(?:[ \t]*\1){2,}[ \t]*$/u.test(firstLine);
51
+ }
52
+ function hasPotentialFrontMatterAmbiguity(markdown) {
53
+ const lines = markdown.replace(/\n$/u, '').split('\n');
54
+ if (lines[0] !== '---') {
55
+ return false;
56
+ }
57
+ const closingDelimiterIndex = lines.indexOf('---', 1);
58
+ return closingDelimiterIndex > 1 && (isBlankLine(lines[1]) || isBlankLine(lines[closingDelimiterIndex - 1]));
20
59
  }
21
60
  function trimBlankEdges(markdown) {
22
61
  const lines = markdown.split('\n');
@@ -1,4 +1,5 @@
1
1
  import type { WrapOptions } from './wrap-options.js';
2
+ export declare function normalizeLineTerminators(text: string): string;
2
3
  export declare function getPreferredNewline(text: string, options: WrapOptions): string;
3
4
  export declare function getLinePrefix(text: string, index: number): string;
4
5
  export declare function getLineStart(text: string, index: number): number;
@@ -1,3 +1,6 @@
1
+ export function normalizeLineTerminators(text) {
2
+ return text.replace(/\r\n|[\r\u2028\u2029]/gu, '\n');
3
+ }
1
4
  export function getPreferredNewline(text, options) {
2
5
  if (options.endOfLine === 'crlf') {
3
6
  return '\r\n';
@@ -1,2 +1,3 @@
1
+ export declare function isEcmaScriptHorizontalWhitespace(character: string): boolean;
1
2
  export declare function skipWhitespace(text: string, index: number): number;
2
3
  export declare function trimWhitespaceEnd(text: string, start: number, end: number): number;
@@ -1,3 +1,7 @@
1
+ const ECMASCRIPT_HORIZONTAL_WHITESPACE_PATTERN = /^[\t\u000b\u000c \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]$/u;
2
+ export function isEcmaScriptHorizontalWhitespace(character) {
3
+ return ECMASCRIPT_HORIZONTAL_WHITESPACE_PATTERN.test(character);
4
+ }
1
5
  export function skipWhitespace(text, index) {
2
6
  let cursor = index;
3
7
  while (cursor < text.length) {