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/README.ja.md +395 -40
- package/README.md +322 -40
- package/lib/cli.d.mts +1 -0
- package/lib/cli.mjs +127 -488
- package/lib/format-file-DSbEElSV.mjs +545 -0
- package/lib/index.d.mts +72 -0
- package/lib/index.mjs +22 -0
- package/package.json +7 -4
package/lib/cli.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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.
|
|
25
|
-
var description = "
|
|
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
|
-
|
|
642
|
-
|
|
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: ["
|
|
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,
|
|
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 --
|
|
676
|
-
else code = await formatFiles(patterns,
|
|
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
|
|
696
|
-
*
|
|
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
|
-
|
|
701
|
-
let
|
|
702
|
-
let
|
|
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 =
|
|
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
|
-
|
|
710
|
-
|
|
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
|
|
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
|
-
|
|
750
|
-
|
|
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: (
|
|
787
|
-
process.stdout.write(
|
|
423
|
+
stdout: (output) => {
|
|
424
|
+
process.stdout.write(output);
|
|
788
425
|
},
|
|
789
|
-
stderr: (
|
|
790
|
-
process.stderr.write(
|
|
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 {};
|