mmdfmt 0.1.0 → 0.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.
package/lib/cli.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { readFile, stat, writeFile } from "node:fs/promises";
2
+ import { a as kindOf, i as formatFile, n as endingsInWords } from "./format-file-DSbEElSV.mjs";
3
3
  import path from "node:path";
4
+ import { getFileInfo } from "prettier";
5
+ import { text } from "node:stream/consumers";
6
+ import { readFile, stat, writeFile } from "node:fs/promises";
7
+ import { styleText } from "node:util";
4
8
  import { cli, define, isArgsValidationError } from "gunshi";
5
- import { format, getFileInfo, resolveConfig } from "prettier";
6
- import { ParseError, parse } from "@mmd-toolkit/parser";
7
- import plugin from "prettier-plugin-mmd";
8
- import { parsers } from "prettier/plugins/markdown";
9
9
  import { convertPathToPattern, glob } from "tinyglobby";
10
10
  //#region src/broken-pipe.ts
11
11
  /**
@@ -21,451 +21,8 @@ function ignoreBrokenPipe(error) {
21
21
  }
22
22
  //#endregion
23
23
  //#region package.json
24
- var version = "0.1.0";
25
- var description = "Command-line formatter for Mermaid diagrams in .mmd files and in Markdown code blocks, which changes nothing else";
26
- //#endregion
27
- //#region src/diagram.ts
28
- /** Prettier's name of each line ending. */
29
- const endOfLineOptions = {
30
- "\n": "lf",
31
- "\r\n": "crlf",
32
- "\r": "cr"
33
- };
34
- /**
35
- * Formats the text of a diagram with Prettier and prettier-plugin-mmd, as
36
- * Prettier formats a `.mmd` file with the plugin. The text has no byte order
37
- * mark and its lines end with `\n`, as Prettier leaves a text before parsing
38
- * it; the lines of the result end with `lineEnding`.
39
- *
40
- * A text that the plugin rejects is parsed again with the parser that the
41
- * plugin uses, for the message and the offset of the syntax error: the error
42
- * that Prettier throws has a line and a column counted for Prettier's code
43
- * frame, which also ends lines at U+2028 and U+2029, and no offset.
44
- */
45
- async function formatDiagram(text, indentation, lineEnding) {
46
- try {
47
- return {
48
- kind: "formatted",
49
- text: await format(text, {
50
- parser: "mermaid",
51
- plugins: [plugin],
52
- tabWidth: indentation.tabWidth,
53
- useTabs: indentation.useTabs,
54
- endOfLine: endOfLineOptions[lineEnding]
55
- })
56
- };
57
- } catch (error) {
58
- const syntaxError = syntaxErrorOf(text);
59
- if (syntaxError === void 0) throw error;
60
- return {
61
- kind: "rejected",
62
- message: syntaxError.message,
63
- offset: syntaxError.range[0]
64
- };
65
- }
66
- }
67
- /** The syntax error of a text, or `undefined` when the parser accepts it. */
68
- function syntaxErrorOf(text) {
69
- try {
70
- parse(text);
71
- } catch (error) {
72
- if (error instanceof ParseError) return error;
73
- throw error;
74
- }
75
- }
76
- //#endregion
77
- //#region src/lines.ts
78
- const byteOrderMark = "";
79
- /** Reads a text into its byte order mark, its lines, and its normalized form. */
80
- function readText(text) {
81
- const mark = text.startsWith(byteOrderMark) ? byteOrderMark : "";
82
- const lines = splitLines(text.slice(mark.length));
83
- return {
84
- byteOrderMark: mark,
85
- lines,
86
- normalized: lines.map((line) => line.text).join("\n")
87
- };
88
- }
89
- /**
90
- * Splits a text into lines at `\n`, `\r\n`, and `\r`. Joining the lines with
91
- * their endings gives the text back; the last line is `""` when the text ends
92
- * with a line terminator.
93
- */
94
- function splitLines(text) {
95
- const lines = [];
96
- let start = 0;
97
- for (let index = 0; index < text.length; index++) {
98
- const character = text[index];
99
- if (character !== "\n" && character !== "\r") continue;
100
- const ending = character === "\r" && text[index + 1] === "\n" ? "\r\n" : character;
101
- lines.push({
102
- text: text.slice(start, index),
103
- ending
104
- });
105
- index += ending.length - 1;
106
- start = index + 1;
107
- }
108
- lines.push({
109
- text: text.slice(start),
110
- ending: ""
111
- });
112
- return lines;
113
- }
114
- /**
115
- * The line ending that the tool writes in a file: the first line ending of the
116
- * file, or `\n` in a file without one (README, Code blocks in detail).
117
- */
118
- function lineEndingOf(text) {
119
- for (let index = 0; index < text.length; index++) {
120
- if (text[index] === "\n") return "\n";
121
- if (text[index] === "\r") return text[index + 1] === "\n" ? "\r\n" : "\r";
122
- }
123
- return "\n";
124
- }
125
- /** The position of an offset in a text whose lines end with `\n`. */
126
- function positionIn(text, offset) {
127
- let line = 1;
128
- let lineStart = 0;
129
- for (let index = 0; index < offset; index++) if (text[index] === "\n") {
130
- line++;
131
- lineStart = index + 1;
132
- }
133
- return {
134
- line,
135
- column: offset - lineStart + 1
136
- };
137
- }
138
- //#endregion
139
- //#region src/markdown.ts
140
- /**
141
- * The first words of an info string that mark a diagram: the `aliases` of the
142
- * language of prettier-plugin-mmd, which are the code blocks that Prettier
143
- * formats with the plugin. Prettier compares them case-sensitively.
144
- */
145
- const diagramLanguages = /* @__PURE__ */ new Set(["mermaid", "mmd"]);
146
- /**
147
- * Formats the diagrams in the code blocks of a Markdown file and changes
148
- * nothing else (README, Code blocks in detail). The warnings give their
149
- * positions in the text that `positionsIn` names.
150
- */
151
- async function formatMarkdown(text, indentation, positionsIn) {
152
- const { byteOrderMark, lines, normalized } = readText(text);
153
- const lineEnding = lineEndingOf(text);
154
- const diagnostics = [];
155
- let output = byteOrderMark;
156
- let next = 0;
157
- let addedLines = 0;
158
- for (const block of diagramBlocksOf(await parseMarkdown(normalized))) {
159
- const result = await formatBlock(block, lines, indentation, lineEnding);
160
- if (result.kind === "warning") {
161
- const { diagnostic } = result;
162
- const { line, column } = diagnostic.position;
163
- diagnostics.push(positionsIn === "input" ? diagnostic : {
164
- ...diagnostic,
165
- position: {
166
- line: line + addedLines,
167
- column
168
- }
169
- });
170
- continue;
171
- }
172
- if (result.kind === "unchanged") continue;
173
- output += joinLines(lines.slice(next, result.start)) + result.text;
174
- next = result.start + result.count;
175
- addedLines += result.textLineCount - result.count;
176
- }
177
- output += joinLines(lines.slice(next));
178
- return {
179
- text: output,
180
- diagnostics
181
- };
182
- }
183
- /**
184
- * Parses Markdown with Prettier's Markdown parser, so that the tool finds the
185
- * code blocks that Prettier finds. Its `parse` reads only the text (it takes
186
- * one parameter), so it is called without Prettier's options.
187
- */
188
- async function parseMarkdown(text) {
189
- const parse = parsers.markdown.parse;
190
- return parse(text);
191
- }
192
- /**
193
- * The code blocks of diagrams in a Markdown tree, in the order of the file,
194
- * without those that Prettier leaves as written for a `prettier-ignore`
195
- * comment (README, Excluding files): the node right after
196
- * `<!-- prettier-ignore -->` among the children of any node, and the nodes
197
- * between `<!-- prettier-ignore-start -->` and the next
198
- * `<!-- prettier-ignore-end -->` among the children of the root.
199
- */
200
- function diagramBlocksOf(root) {
201
- const blocks = [];
202
- /** Collects the blocks of a node and of its descendants. */
203
- function visit(node, footnoteLines) {
204
- if (node.type === "code" && typeof node.lang === "string" && diagramLanguages.has(node.lang) && typeof node.value === "string") {
205
- blocks.push({
206
- value: node.value,
207
- position: node.position,
208
- followsFootnoteLabel: footnoteLines.includes(node.position.start.line)
209
- });
210
- return;
211
- }
212
- const lines = node.type === "footnoteDefinition" ? [...footnoteLines, node.position.start.line] : footnoteLines;
213
- const children = node.children ?? [];
214
- const ignored = node === root ? nodesInIgnoredRanges(children) : /* @__PURE__ */ new Set();
215
- for (const [index, child] of children.entries()) {
216
- if (ignored.has(child)) continue;
217
- if (index > 0 && prettierIgnoreOf(children[index - 1]) === "next") continue;
218
- visit(child, lines);
219
- }
220
- }
221
- visit(root, []);
222
- return blocks;
223
- }
224
- /**
225
- * The nodes between a `prettier-ignore-start` comment and the next
226
- * `prettier-ignore-end` comment, as Prettier finds them among the children of
227
- * the root: a start without an end leaves nothing as written.
228
- */
229
- function nodesInIgnoredRanges(children) {
230
- const ignored = /* @__PURE__ */ new Set();
231
- let start;
232
- for (const [index, child] of children.entries()) {
233
- const ignore = prettierIgnoreOf(child);
234
- if (ignore === "start" && start === void 0) start = index;
235
- else if (ignore === "end" && start !== void 0) {
236
- for (const node of children.slice(start + 1, index)) ignored.add(node);
237
- start = void 0;
238
- }
239
- }
240
- return ignored;
241
- }
242
- /**
243
- * What a node asks as a `prettier-ignore` comment, as Prettier's Markdown
244
- * printer reads it: an HTML node of `<!--`, `prettier-ignore` with
245
- * `-start` or `-end` or nothing, and `-->`, with whitespace around the word.
246
- */
247
- function prettierIgnoreOf(node) {
248
- const { value } = node;
249
- if (node.type !== "html" || value === void 0 || !value.startsWith("<!--") || !value.endsWith("-->")) return;
250
- switch (value.slice(4, -3).trim()) {
251
- case "prettier-ignore": return "next";
252
- case "prettier-ignore-start": return "start";
253
- case "prettier-ignore-end": return "end";
254
- default: return;
255
- }
256
- }
257
- /**
258
- * Formats the diagram of a code block. Only the content lines change: the
259
- * lines between the opening fence and the closing fence, or the end of the
260
- * block, without its blank lines there, when it is not closed. The line of the
261
- * opening fence keeps its text, and its line ending too unless the diagram
262
- * becomes empty.
263
- */
264
- async function formatBlock(block, lines, indentation, lineEnding) {
265
- const start = block.position.start.line;
266
- if (block.followsFootnoteLabel) return {
267
- kind: "warning",
268
- diagnostic: {
269
- severity: "warning",
270
- message: "not formatted: the opening fence stands on the line of a footnote label",
271
- position: {
272
- line: start,
273
- column: block.position.start.column
274
- }
275
- }
276
- };
277
- const contentValues = valueLinesOf(block, lines);
278
- const values = isClosed(block, lines, contentValues.length) ? contentValues : withoutTrailingBlankLines(contentValues);
279
- const prefixes = [];
280
- for (const [index, value] of values.entries()) {
281
- const line = lines[start + index].text;
282
- if (!line.endsWith(value)) return {
283
- kind: "warning",
284
- diagnostic: {
285
- severity: "warning",
286
- message: "not formatted: a tab in the indentation of this line is read partly as the text of the diagram",
287
- position: {
288
- line: start + index + 1,
289
- column: line.length - commonSuffixLength(line, value)
290
- }
291
- }
292
- };
293
- prefixes.push(line.slice(0, line.length - value.length));
294
- }
295
- const endsWithLineEnding = values.length > 0 && lines[start + values.length - 1].ending !== "";
296
- const text = values.join("\n") + (endsWithLineEnding ? "\n" : "");
297
- const result = await formatDiagram(text, indentation, "\n");
298
- if (result.kind === "rejected") {
299
- const { line, column } = positionIn(text, result.offset);
300
- return {
301
- kind: "warning",
302
- diagnostic: {
303
- severity: "warning",
304
- message: result.message,
305
- position: {
306
- line: start + line,
307
- column: (prefixes[line - 1]?.length ?? 0) + column
308
- }
309
- }
310
- };
311
- }
312
- const formatted = !endsWithLineEnding && result.text.endsWith("\n") ? result.text.slice(0, -1) : result.text;
313
- if (formatted === text) return { kind: "unchanged" };
314
- const fenceLine = lines[start - 1];
315
- const prefix = newLinePrefixOf(fenceLine.text.slice(0, block.position.start.column - 1));
316
- const written = [fenceLine];
317
- for (const line of splitLines(formatted)) {
318
- if (line.ending === "" && line.text === "") break;
319
- written.push({
320
- text: line.text === "" ? prefix.trimEnd() : prefix + line.text,
321
- ending: lineEnding
322
- });
323
- }
324
- const last = written.length - 1;
325
- written[last] = {
326
- text: written[last].text,
327
- ending: lines[start + values.length - 1].ending
328
- };
329
- return {
330
- kind: "replaced",
331
- start: start - 1,
332
- count: values.length + 1,
333
- text: joinLines(written),
334
- textLineCount: written.length
335
- };
336
- }
337
- /**
338
- * The lines of the text of a block, one for each content line. The parser
339
- * joins the content lines with `\n`, without a line ending after the last, so
340
- * that the text `""` stands both for no content line and for one empty
341
- * content line; see {@link hasOneEmptyLine}.
342
- */
343
- function valueLinesOf(block, lines) {
344
- if (block.value !== "") return block.value.split("\n");
345
- return hasOneEmptyLine(block, lines) ? [""] : [];
346
- }
347
- /**
348
- * Whether a block whose text is `""` has one content line. The line after the
349
- * opening fence is then that line, which the block reaches into and which
350
- * holds only the indentation and the `>` of the block's container. Otherwise
351
- * that line is the closing fence, which holds the characters of a fence, or it
352
- * is outside the block.
353
- */
354
- function hasOneEmptyLine(block, lines) {
355
- const next = block.position.start.line;
356
- const { end } = block.position;
357
- if (!(end.line - 1 > next || end.line - 1 === next && end.column > 1)) return false;
358
- const line = lines[next].text;
359
- return !line.includes("`") && !line.includes("~");
360
- }
361
- /**
362
- * Whether a block has a closing fence: the line after its content lines, which
363
- * the block reaches into with the characters of a fence. A block that is not
364
- * closed ends where its container or the file ends, on its last content line,
365
- * or on the next line with no more of it than the indentation and the `>` of
366
- * its container.
367
- */
368
- function isClosed(block, lines, contentLineCount) {
369
- const next = block.position.start.line + contentLineCount;
370
- const { end } = block.position;
371
- if (end.line - 1 !== next) return false;
372
- const reached = lines[next].text.slice(0, end.column - 1);
373
- return reached.includes("`") || reached.includes("~");
374
- }
375
- /**
376
- * The lines without the lines at their end that are blank as Markdown reads
377
- * them: empty, or of only spaces and tabs.
378
- */
379
- function withoutTrailingBlankLines(values) {
380
- let count = values.length;
381
- while (count > 0 && isBlank(values[count - 1])) count--;
382
- return values.slice(0, count);
383
- }
384
- /** Whether a line is empty or holds only spaces and tabs. */
385
- function isBlank(text) {
386
- for (const character of text) if (character !== " " && character !== " ") return false;
387
- return true;
388
- }
389
- /**
390
- * What starts a new content line of a block: what stands before the opening
391
- * fence on its line, with the characters of a list marker turned into spaces,
392
- * so that the line takes the indentation and the `>` of the block's container
393
- * and the indentation of the fence (README, Code blocks in detail). Tabs stay as
394
- * written, and with them the columns. A prefix that ends with `>` takes a
395
- * space: a block quote reads one space after its `>` as part of its marker, so
396
- * without it a line of the diagram that starts with a space would lose it.
397
- */
398
- function newLinePrefixOf(beforeFence) {
399
- let prefix = "";
400
- for (const character of beforeFence) prefix += character === " " || character === " " || character === ">" ? character : " ";
401
- return prefix.endsWith(">") ? `${prefix} ` : prefix;
402
- }
403
- /** The number of characters at the end of `a` that `b` ends with too. */
404
- function commonSuffixLength(a, b) {
405
- let length = 0;
406
- while (length < a.length && length < b.length && a[a.length - 1 - length] === b[b.length - 1 - length]) length++;
407
- return length;
408
- }
409
- /** Joins lines with their own line endings. */
410
- function joinLines(lines) {
411
- let text = "";
412
- for (const line of lines) text += line.text + line.ending;
413
- return text;
414
- }
415
- //#endregion
416
- //#region src/format-file.ts
417
- /**
418
- * The files that the tool formats, by the ending of their names (README,
419
- * Patterns).
420
- */
421
- const fileKinds = /* @__PURE__ */ new Map([
422
- [".md", "markdown"],
423
- [".markdown", "markdown"],
424
- [".mmd", "diagram"],
425
- [".mermaid", "diagram"]
426
- ]);
427
- /** The endings of the names of the files that the tool formats, for messages. */
428
- const fileEndings = [...fileKinds.keys()];
429
- /** How the tool reads a file, or `undefined` for a file it does not format. */
430
- function kindOf(file) {
431
- return fileKinds.get(path.extname(file));
432
- }
433
- /**
434
- * Formats a file of the given kind. The indentation comes from the Prettier
435
- * configuration of the file, found as Prettier's CLI finds it,
436
- * `.editorconfig` included (README, Indentation). The diagnostics give their
437
- * positions in the text that `positionsIn` names.
438
- */
439
- async function formatFile(text, file, kind, positionsIn) {
440
- const config = await resolveConfig(file, { editorconfig: true });
441
- const indentation = {
442
- tabWidth: config?.tabWidth,
443
- useTabs: config?.useTabs
444
- };
445
- return kind === "diagram" ? formatDiagramFile(text, indentation) : formatMarkdown(text, indentation, positionsIn);
446
- }
447
- /**
448
- * Formats a `.mmd` or `.mermaid` file as a whole. Prettier keeps a byte order
449
- * mark apart from the text it formats and puts it back; so does this. A
450
- * rejected diagram leaves the text as it was read, so its position is the same
451
- * in the text as read and after formatting.
452
- */
453
- async function formatDiagramFile(text, indentation) {
454
- const { byteOrderMark, normalized } = readText(text);
455
- const result = await formatDiagram(normalized, indentation, lineEndingOf(text));
456
- if (result.kind === "rejected") return {
457
- text,
458
- diagnostics: [{
459
- severity: "error",
460
- message: result.message,
461
- position: positionIn(normalized, result.offset)
462
- }]
463
- };
464
- return {
465
- text: byteOrderMark + result.text,
466
- diagnostics: []
467
- };
468
- }
24
+ var version = "0.2.0";
25
+ var description = "Formatter for Mermaid diagrams in .mmd files and in Markdown code blocks, which changes nothing else, as a command and as a function";
469
26
  //#endregion
470
27
  //#region src/patterns.ts
471
28
  /**
@@ -628,8 +185,6 @@ const exitCodes = {
628
185
  const examples = `mmdfmt --write "**/*.{md,mmd,mermaid}"
629
186
  mmdfmt --check "**/*.{md,mmd,mermaid}"
630
187
  mmdfmt --stdin-filepath diagram.mmd < diagram.mmd`;
631
- /** The endings of the files that the tool formats, as a message names them. */
632
- const endingsInWords = `${fileEndings.slice(0, -1).join(", ")}, or ${fileEndings.at(-1)}`;
633
188
  /** Runs the tool with its arguments, and returns the exit code (README, Usage). */
634
189
  async function main(args, io) {
635
190
  let code = exitCodes.ok;
@@ -638,18 +193,27 @@ async function main(args, io) {
638
193
  args: {
639
194
  write: {
640
195
  type: "boolean",
641
- description: "Rewrite the files that change, and list them",
642
- conflicts: ["check", "stdinFilepath"]
196
+ short: "w",
197
+ negatable: true,
198
+ description: "Rewrite the files that change, and list each file with its time"
643
199
  },
644
200
  check: {
645
201
  type: "boolean",
202
+ short: "c",
203
+ negatable: true,
204
+ description: "Check whether the files are formatted, and print a summary and the files that are not",
205
+ conflicts: ["listDifferent"]
206
+ },
207
+ listDifferent: {
208
+ type: "boolean",
209
+ short: "l",
210
+ negatable: true,
646
211
  description: "List the files that --write would change",
647
- conflicts: ["write", "stdinFilepath"]
212
+ conflicts: ["check"]
648
213
  },
649
214
  stdinFilepath: {
650
215
  type: "string",
651
- description: "Format standard input as the file at this path, and write the result to standard output",
652
- conflicts: ["write", "check"]
216
+ description: "Format standard input as the file at this path, and write the result to standard output"
653
217
  },
654
218
  patterns: {
655
219
  type: "positional",
@@ -667,13 +231,20 @@ async function main(args, io) {
667
231
  code = exitCodes.error;
668
232
  return;
669
233
  }
670
- const { write, check, stdinFilepath } = ctx.values;
234
+ const { write, check, listDifferent, stdinFilepath } = ctx.values;
671
235
  const patterns = [...ctx.values.patterns ?? [], ...ctx.rest];
672
- if (stdinFilepath !== void 0) code = patterns.length > 0 ? argumentError(io, ["--stdin-filepath takes no patterns"]) : await formatStandardInput(stdinFilepath, io);
236
+ if (stdinFilepath !== void 0) code = patterns.length > 0 ? argumentError(io, ["--stdin-filepath takes no patterns"]) : await formatStandardInput(stdinFilepath, {
237
+ write: Boolean(write),
238
+ listsDifference: Boolean(check) || Boolean(listDifferent)
239
+ }, io);
673
240
  else if (ctx.explicit.stdinFilepath) code = argumentError(io, ["--stdin-filepath needs a path"]);
674
241
  else if (patterns.length === 0) code = argumentError(io, ["no patterns of the files to format"]);
675
- else if (!write && !check) code = argumentError(io, ["give --write or --check with the patterns"]);
676
- else code = await formatFiles(patterns, write ? "write" : "check", io);
242
+ else if (!write && !check && !listDifferent) code = argumentError(io, ["give --write, --check, or --list-different with the patterns"]);
243
+ else code = await formatFiles(patterns, {
244
+ write: Boolean(write),
245
+ check: Boolean(check),
246
+ listDifferent: Boolean(listDifferent)
247
+ }, io);
677
248
  }
678
249
  });
679
250
  try {
@@ -692,45 +263,85 @@ async function main(args, io) {
692
263
  return code;
693
264
  }
694
265
  /**
695
- * Formats the files that the patterns select. With `write`, a file that
696
- * changes is rewritten; with `check`, it is only listed.
266
+ * Formats the files that the patterns select, following Prettier's counts
267
+ * and reports for `write`, `check`, and `listDifferent`.
697
268
  */
698
269
  async function formatFiles(patterns, mode, io) {
270
+ if (mode.check) io.stdout("Checking formatting...\n");
699
271
  const { files, noSelectingPattern, skipped, unmatched, excluded } = await selectFiles(patterns, io.cwd);
700
- if (noSelectingPattern) return argumentError(io, [skipped.length > 0 ? "no patterns of the files to format; a pattern in node_modules or a directory of version control is skipped" : "no patterns of the files to format; a pattern that starts with ! only excludes files"]);
701
- let failed = unmatched.length > 0 || excluded.length > 0;
702
- let changed = false;
272
+ let failed = noSelectingPattern || unmatched.length > 0 || excluded.length > 0;
273
+ let numberOfUnformattedFilesFound = 0;
274
+ let numberOfFilesWithError = 0;
275
+ if (noSelectingPattern) argumentError(io, [skipped.length > 0 ? "no patterns of the files to format; a pattern in node_modules or a directory of version control is skipped" : "no patterns of the files to format; a pattern that starts with ! only excludes files"]);
703
276
  for (const pattern of unmatched) io.stderr(`mmdfmt: error: no file ending in ${endingsInWords} matches ${JSON.stringify(pattern)}\n`);
704
277
  for (const pattern of excluded) io.stderr(`mmdfmt: error: a pattern that starts with ! excludes the file ${JSON.stringify(pattern)}\n`);
705
278
  for (const file of files) {
706
- const name = path.relative(io.cwd, file.path);
279
+ const name = displayName(file.path, io.cwd);
280
+ let text, result;
707
281
  try {
708
282
  if (await isSkipped(file.path, io.cwd)) continue;
709
- const text = await readFile(file.path, "utf8");
710
- const result = await formatFile(text, file.path, file.kind, mode === "write" ? "output" : "input");
283
+ text = await readFile(file.path, "utf8");
284
+ } catch (error) {
285
+ io.stderr(`${name}: error: ${messageOf(error)}\n`);
286
+ failed = true;
287
+ continue;
288
+ }
289
+ const start = performance.now();
290
+ try {
291
+ result = await formatFile(text, file.path, file.kind, mode.write ? "output" : "input");
711
292
  report(io, name, result.diagnostics);
712
293
  if (result.diagnostics.some(isError)) {
713
294
  failed = true;
295
+ numberOfFilesWithError += 1;
714
296
  continue;
715
297
  }
716
- if (result.text === text) continue;
717
- if (mode === "write") await writeFile(file.path, result.text);
718
- changed = true;
719
- io.stdout(`${name}\n`);
720
298
  } catch (error) {
721
299
  io.stderr(`${name}: error: ${messageOf(error)}\n`);
722
300
  failed = true;
301
+ numberOfFilesWithError += 1;
302
+ continue;
303
+ }
304
+ const listsTimes = mode.write && !mode.check && !mode.listDifferent;
305
+ const time = `${Math.round(performance.now() - start)}ms`;
306
+ if (result.text === text) {
307
+ if (listsTimes) io.stdout(`${style(io.stdoutStream, "gray", name)} ${time} (unchanged)\n`);
308
+ continue;
309
+ }
310
+ if (mode.write) {
311
+ if (listsTimes) io.stdout(`${name} ${time}\n`);
312
+ try {
313
+ await writeFile(file.path, result.text);
314
+ } catch (error) {
315
+ io.stderr(`${name}: error: ${messageOf(error)}\n`);
316
+ failed = true;
317
+ }
318
+ }
319
+ if (mode.check) warn(io, name);
320
+ else if (mode.listDifferent) io.stdout(`${name}\n`);
321
+ numberOfUnformattedFilesFound += 1;
322
+ }
323
+ if (mode.check) {
324
+ if (numberOfFilesWithError > 0) {
325
+ const filesDescription = numberOfFilesWithError === 1 ? "the above file" : `${numberOfFilesWithError} files`;
326
+ io.stdout(`Error occurred when checking code style in ${filesDescription}.\n`);
327
+ } else if (numberOfUnformattedFilesFound === 0) io.stdout("All diagrams in the matched files use Prettier code style!\n");
328
+ else {
329
+ const filesDescription = numberOfUnformattedFilesFound === 1 ? "the above file" : `${numberOfUnformattedFilesFound} files`;
330
+ warn(io, mode.write ? `Code style issues fixed in ${filesDescription}.` : `Code style issues found in ${filesDescription}. Run mmdfmt with --write to fix.`);
723
331
  }
724
332
  }
725
333
  if (failed) return exitCodes.error;
726
- return mode === "check" && changed ? exitCodes.changed : exitCodes.ok;
334
+ return numberOfUnformattedFilesFound > 0 && !mode.write ? exitCodes.changed : exitCodes.ok;
727
335
  }
728
336
  /**
729
337
  * Formats standard input as the file at `filepath`, and writes the result to
730
338
  * standard output. A skipped path gets its input back unchanged, and a
731
- * rejected diagram file gets nothing.
339
+ * rejected diagram file gets nothing. With `listsDifference` (`--check` or
340
+ * `--list-different`), the result is not written: `(stdin)` is listed when it
341
+ * differs from the input, unless `write` is given too, as Prettier lists it.
342
+ * `write` alone changes nothing: no file is written.
732
343
  */
733
- async function formatStandardInput(filepath, io) {
344
+ async function formatStandardInput(filepath, mode, io) {
734
345
  const file = path.resolve(io.cwd, filepath);
735
346
  const kind = kindOf(file);
736
347
  if (kind === void 0) {
@@ -743,16 +354,45 @@ async function formatStandardInput(filepath, io) {
743
354
  io.stdout(text);
744
355
  return exitCodes.ok;
745
356
  }
746
- const result = await formatFile(text, file, kind, "output");
357
+ const result = await formatFile(text, file, kind, mode.listsDifference ? "input" : "output");
747
358
  report(io, filepath, result.diagnostics);
748
359
  if (result.diagnostics.some(isError)) return exitCodes.error;
749
- io.stdout(result.text);
750
- return exitCodes.ok;
360
+ if (!mode.listsDifference) {
361
+ io.stdout(result.text);
362
+ return exitCodes.ok;
363
+ }
364
+ if (result.text === text || mode.write) return exitCodes.ok;
365
+ io.stdout("(stdin)\n");
366
+ return exitCodes.changed;
751
367
  } catch (error) {
752
368
  io.stderr(`${filepath}: error: ${messageOf(error)}\n`);
753
369
  return exitCodes.error;
754
370
  }
755
371
  }
372
+ /**
373
+ * Writes a warning of `--check` to standard error, as Prettier's logger
374
+ * writes one: `[warn] ` before the text, with the word `warn` in yellow where
375
+ * the stream shows colors.
376
+ */
377
+ function warn(io, text) {
378
+ io.stderr(`[${style(io.stderrStream, "yellow", "warn")}] ${text}\n`);
379
+ }
380
+ /**
381
+ * `text` in `color` where Node's `util.styleText` colors text for `stream`,
382
+ * and as it is without a stream.
383
+ */
384
+ function style(stream, color, text) {
385
+ return stream === void 0 ? text : styleText(color, text, { stream });
386
+ }
387
+ /**
388
+ * The name of a file in what the tool prints: its path relative to `cwd`, with
389
+ * `/` between its parts on every system, as Prettier's CLI writes the name of a
390
+ * file (README, Usage). `pathOf` is `node:path` of the system the tool runs on;
391
+ * a test gives `path.win32` or `path.posix`.
392
+ */
393
+ function displayName(file, cwd, pathOf = path) {
394
+ return pathOf.relative(cwd, file).replaceAll(pathOf.sep, "/");
395
+ }
756
396
  /** Writes the diagnostics of a file to standard error. */
757
397
  function report(io, name, diagnostics) {
758
398
  for (const { severity, message, position } of diagnostics) io.stderr(`${name}:${position.line}:${position.column}: ${severity}: ${message}\n`);
@@ -774,21 +414,20 @@ function messageOf(error) {
774
414
  //#region src/cli.ts
775
415
  /** Reads standard input to its end, as UTF-8. */
776
416
  async function readStdin() {
777
- process.stdin.setEncoding("utf8");
778
- let text = "";
779
- for await (const chunk of process.stdin) text += chunk;
780
- return text;
417
+ return text(process.stdin.setEncoding("utf8"));
781
418
  }
782
419
  process.stdout.on("error", ignoreBrokenPipe);
783
420
  process.exitCode = await main(process.argv.slice(2), {
784
421
  cwd: process.cwd(),
785
422
  readStdin,
786
- stdout: (text) => {
787
- process.stdout.write(text);
423
+ stdout: (output) => {
424
+ process.stdout.write(output);
788
425
  },
789
- stderr: (text) => {
790
- process.stderr.write(text);
791
- }
426
+ stderr: (output) => {
427
+ process.stderr.write(output);
428
+ },
429
+ stdoutStream: process.stdout,
430
+ stderrStream: process.stderr
792
431
  });
793
432
  //#endregion
794
433
  export {};