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
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { clearConfigCache, format, getSupportInfo, resolveConfig, util } from "prettier";
|
|
3
|
+
import plugin, { languages } from "prettier-plugin-mmd";
|
|
4
|
+
import { parsers } from "prettier/plugins/markdown";
|
|
5
|
+
//#region src/diagram.ts
|
|
6
|
+
/** The default of a Prettier option that is a number, as Prettier states it. */
|
|
7
|
+
async function defaultNumberOption(name) {
|
|
8
|
+
const { options } = await getSupportInfo();
|
|
9
|
+
const option = options.find((candidate) => candidate.name === name);
|
|
10
|
+
if (typeof option?.default !== "number") throw new Error(`Prettier states no numeric default for ${name}`);
|
|
11
|
+
return option.default;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The line width left for the lines of a diagram that the tool writes after
|
|
15
|
+
* `prefix`, which stands before each of them in a Markdown code block (README,
|
|
16
|
+
* Code blocks in detail). Prettier formats a diagram in a list or a block quote
|
|
17
|
+
* inside the container, so the width of what stands before the diagram is no
|
|
18
|
+
* longer left; here the diagram is formatted alone and the prefix is put in
|
|
19
|
+
* front of its lines. The width of the prefix is counted as Prettier counts the
|
|
20
|
+
* indentation that stands before a range it formats (`util.getAlignmentSize`),
|
|
21
|
+
* a tab reaching the next multiple of `tabWidth`; where `tabWidth` is 0, which
|
|
22
|
+
* `util.getAlignmentSize` turns into NaN for a tab, a tab has no width. The
|
|
23
|
+
* width is at least 1. Prettier refuses a `printWidth` or a `tabWidth` that is
|
|
24
|
+
* not an integer; then `printWidth` is given as it is, so that Prettier
|
|
25
|
+
* refuses the value that was written, as it does for a diagram with no prefix.
|
|
26
|
+
*/
|
|
27
|
+
async function printWidthAfter(prefix, options) {
|
|
28
|
+
if (prefix === "") return options.printWidth;
|
|
29
|
+
const tabWidth = options.tabWidth ?? await defaultNumberOption("tabWidth");
|
|
30
|
+
const printWidth = options.printWidth ?? await defaultNumberOption("printWidth");
|
|
31
|
+
if (!Number.isInteger(tabWidth) || !Number.isInteger(printWidth)) return options.printWidth;
|
|
32
|
+
const used = tabWidth === 0 ? prefix.replaceAll(" ", "").length : util.getAlignmentSize(prefix, tabWidth);
|
|
33
|
+
return Math.max(1, printWidth - used);
|
|
34
|
+
}
|
|
35
|
+
/** Prettier's name of each line ending. */
|
|
36
|
+
const endOfLineOptions = {
|
|
37
|
+
"\n": "lf",
|
|
38
|
+
"\r\n": "crlf",
|
|
39
|
+
"\r": "cr"
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Formats the text of a diagram as {@link formatDiagramText} does, and gives
|
|
43
|
+
* a text that the plugin rejects as the message and the offset of its syntax
|
|
44
|
+
* error.
|
|
45
|
+
*/
|
|
46
|
+
async function formatDiagram(text, options, lineEnding) {
|
|
47
|
+
try {
|
|
48
|
+
return {
|
|
49
|
+
kind: "formatted",
|
|
50
|
+
text: await formatDiagramText(text, options, lineEnding)
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
kind: "rejected",
|
|
55
|
+
...rejectionOf(error)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Formats the text of a diagram with Prettier and prettier-plugin-mmd, as
|
|
61
|
+
* Prettier formats a `.mmd` file with the plugin, and throws what Prettier
|
|
62
|
+
* throws for a text that the plugin rejects. `options` are the options that
|
|
63
|
+
* the tool reads of the Prettier configuration of the file (README,
|
|
64
|
+
* Configuration), given to Prettier together with the three that the tool sets
|
|
65
|
+
* itself: the parser, the plugins, and the line endings. The text has no byte
|
|
66
|
+
* order mark and its lines end with `\n`, as Prettier leaves a text before
|
|
67
|
+
* parsing it; the lines of the result end with `lineEnding`.
|
|
68
|
+
*/
|
|
69
|
+
async function formatDiagramText(text, options, lineEnding) {
|
|
70
|
+
return format(text, {
|
|
71
|
+
...options,
|
|
72
|
+
parser: "mermaid",
|
|
73
|
+
plugins: [plugin],
|
|
74
|
+
endOfLine: endOfLineOptions[lineEnding]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The message and the offset of the syntax error of a text that the plugin
|
|
79
|
+
* rejected, from the error that formatting it threw. That error keeps the
|
|
80
|
+
* parser's error as its `cause`, which gives the message without Prettier's
|
|
81
|
+
* code frame and the offset in the text that was formatted. Any other error is
|
|
82
|
+
* thrown again.
|
|
83
|
+
*/
|
|
84
|
+
function rejectionOf(error) {
|
|
85
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
86
|
+
if (!isParseError(cause)) throw error;
|
|
87
|
+
return {
|
|
88
|
+
message: cause.message,
|
|
89
|
+
offset: cause.range[0]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Whether a value is the parser's syntax error. It is checked by name, as the
|
|
94
|
+
* plugin checks it, so that the check also holds when two copies of the parser
|
|
95
|
+
* are installed.
|
|
96
|
+
*/
|
|
97
|
+
function isParseError(error) {
|
|
98
|
+
return error instanceof Error && error.name === "ParseError";
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/lines.ts
|
|
102
|
+
const byteOrderMark = "";
|
|
103
|
+
/** Reads a text into its byte order mark, its lines, and its normalized form. */
|
|
104
|
+
function readText(text) {
|
|
105
|
+
const mark = text.startsWith(byteOrderMark) ? byteOrderMark : "";
|
|
106
|
+
const lines = splitLines(text.slice(mark.length));
|
|
107
|
+
return {
|
|
108
|
+
byteOrderMark: mark,
|
|
109
|
+
lines,
|
|
110
|
+
normalized: lines.map((line) => line.text).join("\n")
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Splits a text into lines at `\n`, `\r\n`, and `\r`. Joining the lines with
|
|
115
|
+
* their endings gives the text back; the last line is `""` when the text ends
|
|
116
|
+
* with a line terminator.
|
|
117
|
+
*/
|
|
118
|
+
function splitLines(text) {
|
|
119
|
+
const lines = [];
|
|
120
|
+
let start = 0;
|
|
121
|
+
for (let index = 0; index < text.length; index++) {
|
|
122
|
+
const character = text[index];
|
|
123
|
+
if (character !== "\n" && character !== "\r") continue;
|
|
124
|
+
const ending = character === "\r" && text[index + 1] === "\n" ? "\r\n" : character;
|
|
125
|
+
lines.push({
|
|
126
|
+
text: text.slice(start, index),
|
|
127
|
+
ending
|
|
128
|
+
});
|
|
129
|
+
index += ending.length - 1;
|
|
130
|
+
start = index + 1;
|
|
131
|
+
}
|
|
132
|
+
lines.push({
|
|
133
|
+
text: text.slice(start),
|
|
134
|
+
ending: ""
|
|
135
|
+
});
|
|
136
|
+
return lines;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The line ending that the tool writes in a file: the first line ending of the
|
|
140
|
+
* file, or `\n` in a file without one (README, Code blocks in detail).
|
|
141
|
+
*/
|
|
142
|
+
function lineEndingOf(text) {
|
|
143
|
+
for (let index = 0; index < text.length; index++) {
|
|
144
|
+
if (text[index] === "\n") return "\n";
|
|
145
|
+
if (text[index] === "\r") return text[index + 1] === "\n" ? "\r\n" : "\r";
|
|
146
|
+
}
|
|
147
|
+
return "\n";
|
|
148
|
+
}
|
|
149
|
+
/** The position of an offset in a text whose lines end with `\n`. */
|
|
150
|
+
function positionIn(text, offset) {
|
|
151
|
+
let line = 1;
|
|
152
|
+
let lineStart = 0;
|
|
153
|
+
for (let index = 0; index < offset; index++) if (text[index] === "\n") {
|
|
154
|
+
line++;
|
|
155
|
+
lineStart = index + 1;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
line,
|
|
159
|
+
column: offset - lineStart + 1
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/markdown.ts
|
|
164
|
+
/**
|
|
165
|
+
* The first words of an info string that mark a diagram: the `aliases` of the
|
|
166
|
+
* language of prettier-plugin-mmd. Prettier compares them case-sensitively. It
|
|
167
|
+
* also matches the language's name in lower case and its extensions without the
|
|
168
|
+
* dot, which for this plugin are among its aliases;
|
|
169
|
+
* `tests/plugin-blocks.test.ts` compares the blocks with those Prettier formats.
|
|
170
|
+
*/
|
|
171
|
+
const diagramLanguages = new Set(languages.flatMap((language) => language.aliases ?? []));
|
|
172
|
+
/**
|
|
173
|
+
* Formats the diagrams in the code blocks of a Markdown file and changes
|
|
174
|
+
* nothing else (README, Code blocks in detail). The warnings give their
|
|
175
|
+
* positions in the text that `positionsIn` names.
|
|
176
|
+
*/
|
|
177
|
+
async function formatMarkdown(text, options, positionsIn) {
|
|
178
|
+
const { byteOrderMark, lines, normalized } = readText(text);
|
|
179
|
+
const lineEnding = lineEndingOf(text);
|
|
180
|
+
const diagnostics = [];
|
|
181
|
+
let output = byteOrderMark;
|
|
182
|
+
let next = 0;
|
|
183
|
+
let addedLines = 0;
|
|
184
|
+
for (const block of diagramBlocksOf(await parseMarkdown(normalized))) {
|
|
185
|
+
const result = await formatBlock(block, lines, options, lineEnding);
|
|
186
|
+
if (result.kind === "warning") {
|
|
187
|
+
const { diagnostic } = result;
|
|
188
|
+
const { line, column } = diagnostic.position;
|
|
189
|
+
diagnostics.push(positionsIn === "input" ? diagnostic : {
|
|
190
|
+
...diagnostic,
|
|
191
|
+
position: {
|
|
192
|
+
line: line + addedLines,
|
|
193
|
+
column
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (result.kind === "unchanged") continue;
|
|
199
|
+
output += joinLines(lines.slice(next, result.start)) + result.text;
|
|
200
|
+
next = result.start + result.count;
|
|
201
|
+
addedLines += result.textLineCount - result.count;
|
|
202
|
+
}
|
|
203
|
+
output += joinLines(lines.slice(next));
|
|
204
|
+
return {
|
|
205
|
+
text: output,
|
|
206
|
+
diagnostics
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Parses Markdown with Prettier's Markdown parser, so that the tool finds the
|
|
211
|
+
* code blocks that Prettier finds. Its `parse` reads only the text (it takes
|
|
212
|
+
* one parameter), so it is called without Prettier's options.
|
|
213
|
+
*/
|
|
214
|
+
async function parseMarkdown(text) {
|
|
215
|
+
const parse = parsers.markdown.parse;
|
|
216
|
+
return parse(text);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The code blocks of diagrams in a Markdown tree, in the order of the file,
|
|
220
|
+
* without those that Prettier leaves as written for a `prettier-ignore`
|
|
221
|
+
* comment (README, Excluding files): the node right after
|
|
222
|
+
* `<!-- prettier-ignore -->` among the children of any node, and the nodes
|
|
223
|
+
* between `<!-- prettier-ignore-start -->` and the next
|
|
224
|
+
* `<!-- prettier-ignore-end -->` among the children of the root.
|
|
225
|
+
*/
|
|
226
|
+
function diagramBlocksOf(root) {
|
|
227
|
+
const blocks = [];
|
|
228
|
+
/** Collects the blocks of a node and of its descendants. */
|
|
229
|
+
function visit(node, footnoteLines) {
|
|
230
|
+
if (node.type === "code" && typeof node.lang === "string" && diagramLanguages.has(node.lang) && typeof node.value === "string") {
|
|
231
|
+
blocks.push({
|
|
232
|
+
value: node.value,
|
|
233
|
+
position: node.position,
|
|
234
|
+
followsFootnoteLabel: footnoteLines.includes(node.position.start.line)
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const lines = node.type === "footnoteDefinition" ? [...footnoteLines, node.position.start.line] : footnoteLines;
|
|
239
|
+
const children = node.children ?? [];
|
|
240
|
+
const ignored = node === root ? nodesInIgnoredRanges(children) : /* @__PURE__ */ new Set();
|
|
241
|
+
for (const [index, child] of children.entries()) {
|
|
242
|
+
if (ignored.has(child)) continue;
|
|
243
|
+
if (index > 0 && prettierIgnoreOf(children[index - 1]) === "next") continue;
|
|
244
|
+
visit(child, lines);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
visit(root, []);
|
|
248
|
+
return blocks;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The nodes between a `prettier-ignore-start` comment and the next
|
|
252
|
+
* `prettier-ignore-end` comment, as Prettier finds them among the children of
|
|
253
|
+
* the root: a start without an end leaves nothing as written.
|
|
254
|
+
*/
|
|
255
|
+
function nodesInIgnoredRanges(children) {
|
|
256
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
257
|
+
let start;
|
|
258
|
+
for (const [index, child] of children.entries()) {
|
|
259
|
+
const ignore = prettierIgnoreOf(child);
|
|
260
|
+
if (ignore === "start" && start === void 0) start = index;
|
|
261
|
+
else if (ignore === "end" && start !== void 0) {
|
|
262
|
+
for (const node of children.slice(start + 1, index)) ignored.add(node);
|
|
263
|
+
start = void 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return ignored;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* What a node asks as a `prettier-ignore` comment, as Prettier's Markdown
|
|
270
|
+
* printer reads it: an HTML node of `<!--`, `prettier-ignore` with
|
|
271
|
+
* `-start` or `-end` or nothing, and `-->`, with whitespace around the word.
|
|
272
|
+
*/
|
|
273
|
+
function prettierIgnoreOf(node) {
|
|
274
|
+
const { value } = node;
|
|
275
|
+
if (node.type !== "html" || value === void 0 || !value.startsWith("<!--") || !value.endsWith("-->")) return;
|
|
276
|
+
switch (value.slice(4, -3).trim()) {
|
|
277
|
+
case "prettier-ignore": return "next";
|
|
278
|
+
case "prettier-ignore-start": return "start";
|
|
279
|
+
case "prettier-ignore-end": return "end";
|
|
280
|
+
default: return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Formats the diagram of a code block. Only the content lines change: the
|
|
285
|
+
* lines between the opening fence and the closing fence, or the end of the
|
|
286
|
+
* block, without its blank lines there, when it is not closed. The line of the
|
|
287
|
+
* opening fence keeps its text, and its line ending too unless the diagram
|
|
288
|
+
* becomes empty.
|
|
289
|
+
*/
|
|
290
|
+
async function formatBlock(block, lines, options, lineEnding) {
|
|
291
|
+
const start = block.position.start.line;
|
|
292
|
+
if (block.followsFootnoteLabel) return {
|
|
293
|
+
kind: "warning",
|
|
294
|
+
diagnostic: {
|
|
295
|
+
severity: "warning",
|
|
296
|
+
message: "not formatted: the opening fence stands on the line of a footnote label",
|
|
297
|
+
position: {
|
|
298
|
+
line: start,
|
|
299
|
+
column: block.position.start.column
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const contentValues = valueLinesOf(block, lines);
|
|
304
|
+
const values = isClosed(block, lines, contentValues.length) ? contentValues : withoutTrailingBlankLines(contentValues);
|
|
305
|
+
const prefixes = [];
|
|
306
|
+
for (const [index, value] of values.entries()) {
|
|
307
|
+
const line = lines[start + index].text;
|
|
308
|
+
if (!line.endsWith(value)) return {
|
|
309
|
+
kind: "warning",
|
|
310
|
+
diagnostic: {
|
|
311
|
+
severity: "warning",
|
|
312
|
+
message: "not formatted: a tab in the indentation of this line is read partly as the text of the diagram",
|
|
313
|
+
position: {
|
|
314
|
+
line: start + index + 1,
|
|
315
|
+
column: line.length - commonSuffixLength(line, value)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
prefixes.push(line.slice(0, line.length - value.length));
|
|
320
|
+
}
|
|
321
|
+
const endsWithLineEnding = values.length > 0 && lines[start + values.length - 1].ending !== "";
|
|
322
|
+
const text = values.join("\n") + (endsWithLineEnding ? "\n" : "");
|
|
323
|
+
const fenceLine = lines[start - 1];
|
|
324
|
+
const prefix = newLinePrefixOf(fenceLine.text.slice(0, block.position.start.column - 1));
|
|
325
|
+
const result = await formatDiagram(text, {
|
|
326
|
+
...options,
|
|
327
|
+
printWidth: await printWidthAfter(prefix, options)
|
|
328
|
+
}, "\n");
|
|
329
|
+
if (result.kind === "rejected") {
|
|
330
|
+
const { line, column } = positionIn(text, result.offset);
|
|
331
|
+
return {
|
|
332
|
+
kind: "warning",
|
|
333
|
+
diagnostic: {
|
|
334
|
+
severity: "warning",
|
|
335
|
+
message: result.message,
|
|
336
|
+
position: {
|
|
337
|
+
line: start + line,
|
|
338
|
+
column: (prefixes[line - 1]?.length ?? 0) + column
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const formatted = !endsWithLineEnding && result.text.endsWith("\n") ? result.text.slice(0, -1) : result.text;
|
|
344
|
+
if (formatted === text) return { kind: "unchanged" };
|
|
345
|
+
const written = [fenceLine];
|
|
346
|
+
for (const line of splitLines(formatted)) {
|
|
347
|
+
if (line.ending === "" && line.text === "") break;
|
|
348
|
+
written.push({
|
|
349
|
+
text: line.text === "" ? prefix.trimEnd() : prefix + line.text,
|
|
350
|
+
ending: lineEnding
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const last = written.length - 1;
|
|
354
|
+
written[last] = {
|
|
355
|
+
text: written[last].text,
|
|
356
|
+
ending: lines[start + values.length - 1].ending
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
kind: "replaced",
|
|
360
|
+
start: start - 1,
|
|
361
|
+
count: values.length + 1,
|
|
362
|
+
text: joinLines(written),
|
|
363
|
+
textLineCount: written.length
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The lines of the text of a block, one for each content line. The parser
|
|
368
|
+
* joins the content lines with `\n`, without a line ending after the last, so
|
|
369
|
+
* that the text `""` stands both for no content line and for one empty
|
|
370
|
+
* content line; see {@link hasOneEmptyLine}.
|
|
371
|
+
*/
|
|
372
|
+
function valueLinesOf(block, lines) {
|
|
373
|
+
if (block.value !== "") return block.value.split("\n");
|
|
374
|
+
return hasOneEmptyLine(block, lines) ? [""] : [];
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Whether a block whose text is `""` has one content line. The line after the
|
|
378
|
+
* opening fence is then that line, which the block reaches into and which
|
|
379
|
+
* holds only the indentation and the `>` of the block's container. Otherwise
|
|
380
|
+
* that line is the closing fence, which holds the characters of a fence, or it
|
|
381
|
+
* is outside the block.
|
|
382
|
+
*/
|
|
383
|
+
function hasOneEmptyLine(block, lines) {
|
|
384
|
+
const next = block.position.start.line;
|
|
385
|
+
const { end } = block.position;
|
|
386
|
+
if (!(end.line - 1 > next || end.line - 1 === next && end.column > 1)) return false;
|
|
387
|
+
const line = lines[next].text;
|
|
388
|
+
return !line.includes("`") && !line.includes("~");
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Whether a block has a closing fence: the line after its content lines, which
|
|
392
|
+
* the block reaches into with the characters of a fence. A block that is not
|
|
393
|
+
* closed ends where its container or the file ends, on its last content line,
|
|
394
|
+
* or on the next line with no more of it than the indentation and the `>` of
|
|
395
|
+
* its container.
|
|
396
|
+
*/
|
|
397
|
+
function isClosed(block, lines, contentLineCount) {
|
|
398
|
+
const next = block.position.start.line + contentLineCount;
|
|
399
|
+
const { end } = block.position;
|
|
400
|
+
if (end.line - 1 !== next) return false;
|
|
401
|
+
const reached = lines[next].text.slice(0, end.column - 1);
|
|
402
|
+
return reached.includes("`") || reached.includes("~");
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The lines without the lines at their end that are blank as Markdown reads
|
|
406
|
+
* them: empty, or of only spaces and tabs.
|
|
407
|
+
*/
|
|
408
|
+
function withoutTrailingBlankLines(values) {
|
|
409
|
+
let count = values.length;
|
|
410
|
+
while (count > 0 && isBlank(values[count - 1])) count--;
|
|
411
|
+
return values.slice(0, count);
|
|
412
|
+
}
|
|
413
|
+
/** Whether a line is empty or holds only spaces and tabs. */
|
|
414
|
+
function isBlank(text) {
|
|
415
|
+
return util.skipSpaces(text, 0) === text.length;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* What starts a new content line of a block: what stands before the opening
|
|
419
|
+
* fence on its line, with the characters of a list marker turned into spaces,
|
|
420
|
+
* so that the line takes the indentation and the `>` of the block's container
|
|
421
|
+
* and the indentation of the fence (README, Code blocks in detail). Tabs stay as
|
|
422
|
+
* written, and with them the columns. A prefix that ends with `>` takes a
|
|
423
|
+
* space: a block quote reads one space after its `>` as part of its marker, so
|
|
424
|
+
* without it a line of the diagram that starts with a space would lose it.
|
|
425
|
+
*/
|
|
426
|
+
function newLinePrefixOf(beforeFence) {
|
|
427
|
+
let prefix = "";
|
|
428
|
+
for (const character of beforeFence) prefix += character === " " || character === " " || character === ">" ? character : " ";
|
|
429
|
+
return prefix.endsWith(">") ? `${prefix} ` : prefix;
|
|
430
|
+
}
|
|
431
|
+
/** The number of characters at the end of `a` that `b` ends with too. */
|
|
432
|
+
function commonSuffixLength(a, b) {
|
|
433
|
+
let length = 0;
|
|
434
|
+
while (length < a.length && length < b.length && a[a.length - 1 - length] === b[b.length - 1 - length]) length++;
|
|
435
|
+
return length;
|
|
436
|
+
}
|
|
437
|
+
/** Joins lines with their own line endings. */
|
|
438
|
+
function joinLines(lines) {
|
|
439
|
+
let text = "";
|
|
440
|
+
for (const line of lines) text += line.text + line.ending;
|
|
441
|
+
return text;
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/options.ts
|
|
445
|
+
/**
|
|
446
|
+
* The options that the tool reads of a configuration that Prettier resolved for
|
|
447
|
+
* a file, in `config`; the others, such as the range, the pragmas, and the
|
|
448
|
+
* options of other languages, are not read. An option that the configuration
|
|
449
|
+
* lacks is not in the result, as in what Prettier resolves, so that a caller
|
|
450
|
+
* can spread the result over its own defaults.
|
|
451
|
+
*/
|
|
452
|
+
function readOptions(config) {
|
|
453
|
+
const named = {
|
|
454
|
+
printWidth: config.printWidth,
|
|
455
|
+
tabWidth: config.tabWidth,
|
|
456
|
+
useTabs: config.useTabs,
|
|
457
|
+
embeddedLanguageFormatting: config.embeddedLanguageFormatting,
|
|
458
|
+
singleQuote: config.singleQuote,
|
|
459
|
+
bracketSpacing: config.bracketSpacing,
|
|
460
|
+
proseWrap: config.proseWrap,
|
|
461
|
+
trailingComma: config.trailingComma
|
|
462
|
+
};
|
|
463
|
+
return Object.fromEntries(Object.entries(named).filter(([, value]) => value !== void 0));
|
|
464
|
+
}
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region src/format-file.ts
|
|
467
|
+
/**
|
|
468
|
+
* The files that the tool formats, by the ending of their names (README,
|
|
469
|
+
* Patterns).
|
|
470
|
+
*/
|
|
471
|
+
const fileKinds = /* @__PURE__ */ new Map([
|
|
472
|
+
[".md", "markdown"],
|
|
473
|
+
[".markdown", "markdown"],
|
|
474
|
+
[".mmd", "diagram"],
|
|
475
|
+
[".mermaid", "diagram"]
|
|
476
|
+
]);
|
|
477
|
+
/** The endings of the names of the files that the tool formats. */
|
|
478
|
+
const fileEndings = [...fileKinds.keys()];
|
|
479
|
+
/** The endings of the files that the tool formats, as a message names them. */
|
|
480
|
+
const endingsInWords = [...fileEndings.slice(0, -1), `or ${fileEndings.at(-1)}`].join(", ");
|
|
481
|
+
/** How the tool reads a file, or `undefined` for a file it does not format. */
|
|
482
|
+
function kindOf(file) {
|
|
483
|
+
return fileKinds.get(path.extname(file));
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Resolves the configuration of a path or a file URL as the command does,
|
|
487
|
+
* `.editorconfig` included, and returns the options that the command reads
|
|
488
|
+
* from it (README, Configuration), or `null` when no configuration applies
|
|
489
|
+
* (README, From a script).
|
|
490
|
+
*/
|
|
491
|
+
async function resolveConfig$1(fileUrlOrPath) {
|
|
492
|
+
const config = await resolveConfig(fileUrlOrPath, { editorconfig: true });
|
|
493
|
+
return config === null ? null : readOptions(config);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Forgets the configuration files that `resolveConfig` has read, so that the
|
|
497
|
+
* next call reads them again: Prettier keeps them for the life of the process
|
|
498
|
+
* (README, From a script).
|
|
499
|
+
*/
|
|
500
|
+
async function clearConfigCache$1() {
|
|
501
|
+
await clearConfigCache();
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Formats a file of the given kind, with the options that `resolveConfig`
|
|
505
|
+
* returns for it. The diagnostics give their positions in the text that
|
|
506
|
+
* `positionsIn` names.
|
|
507
|
+
*/
|
|
508
|
+
async function formatFile(text, file, kind, positionsIn) {
|
|
509
|
+
const options = await resolveConfig$1(file) ?? {};
|
|
510
|
+
return kind === "diagram" ? formatDiagramFile(text, options) : formatMarkdown(text, options, positionsIn);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Formats a `.mmd` or `.mermaid` file as a whole. Prettier keeps a byte order
|
|
514
|
+
* mark apart from the text it formats and puts it back; so does this. The lines
|
|
515
|
+
* end as the first line of the source does. A failure throws Prettier's error.
|
|
516
|
+
*/
|
|
517
|
+
async function formatDiagramSource(text, options) {
|
|
518
|
+
const { byteOrderMark, normalized } = readText(text);
|
|
519
|
+
return byteOrderMark + await formatDiagramText(normalized, options, lineEndingOf(text));
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Formats a diagram file for the command. A rejected diagram leaves the text
|
|
523
|
+
* as it was read, so its position is the same before and after formatting.
|
|
524
|
+
*/
|
|
525
|
+
async function formatDiagramFile(text, options) {
|
|
526
|
+
try {
|
|
527
|
+
return {
|
|
528
|
+
text: await formatDiagramSource(text, options),
|
|
529
|
+
diagnostics: []
|
|
530
|
+
};
|
|
531
|
+
} catch (error) {
|
|
532
|
+
const { normalized } = readText(text);
|
|
533
|
+
const rejection = rejectionOf(error);
|
|
534
|
+
return {
|
|
535
|
+
text,
|
|
536
|
+
diagnostics: [{
|
|
537
|
+
severity: "error",
|
|
538
|
+
message: rejection.message,
|
|
539
|
+
position: positionIn(normalized, rejection.offset)
|
|
540
|
+
}]
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
//#endregion
|
|
545
|
+
export { kindOf as a, formatMarkdown as c, formatFile as i, endingsInWords as n, resolveConfig$1 as o, formatDiagramSource as r, readOptions as s, clearConfigCache$1 as t };
|
package/lib/index.d.mts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import "prettier";
|
|
2
|
+
//#region src/options.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The Prettier options that the tool reads, and the only ones that it gives to
|
|
5
|
+
* Prettier from a configuration (README, Configuration). An option that
|
|
6
|
+
* Prettier adds is not read until it is written here. The types are the tool's
|
|
7
|
+
* own: where the types of Prettier gain a value for one of these options, as
|
|
8
|
+
* they may in a later version, the type checker fails in `readOptions` until
|
|
9
|
+
* the type here is widened on purpose.
|
|
10
|
+
*/
|
|
11
|
+
interface ReadOptions {
|
|
12
|
+
/**
|
|
13
|
+
* The line width: whether a data block `@{ … }` on one line is printed on
|
|
14
|
+
* several lines, whether the colons of the tasks of a Gantt section are lined
|
|
15
|
+
* up, and where Prettier breaks the YAML of the frontmatter.
|
|
16
|
+
*/
|
|
17
|
+
readonly printWidth?: number;
|
|
18
|
+
/** The width of one level of indentation, and of a tab. */
|
|
19
|
+
readonly tabWidth?: number;
|
|
20
|
+
/** Whether a level of indentation is a tab. */
|
|
21
|
+
readonly useTabs?: boolean;
|
|
22
|
+
/** `"off"` leaves the YAML of the frontmatter as written. */
|
|
23
|
+
readonly embeddedLanguageFormatting?: "auto" | "off";
|
|
24
|
+
/** The quotes of the strings in the YAML of the frontmatter. */
|
|
25
|
+
readonly singleQuote?: boolean;
|
|
26
|
+
/** The spaces inside the braces of a flow mapping in the frontmatter. */
|
|
27
|
+
readonly bracketSpacing?: boolean;
|
|
28
|
+
/** Whether Prettier wraps a long text in the YAML of the frontmatter. */
|
|
29
|
+
readonly proseWrap?: "always" | "never" | "preserve";
|
|
30
|
+
/** The comma after the last entry of a flow collection that breaks. */
|
|
31
|
+
readonly trailingComma?: "all" | "es5" | "none";
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/format-file.d.ts
|
|
35
|
+
/**
|
|
36
|
+
* Resolves the configuration of a path or a file URL as the command does,
|
|
37
|
+
* `.editorconfig` included, and returns the options that the command reads
|
|
38
|
+
* from it (README, Configuration), or `null` when no configuration applies
|
|
39
|
+
* (README, From a script).
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveConfig(fileUrlOrPath: string | URL): Promise<ReadOptions | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Forgets the configuration files that `resolveConfig` has read, so that the
|
|
44
|
+
* next call reads them again: Prettier keeps them for the life of the process
|
|
45
|
+
* (README, From a script).
|
|
46
|
+
*/
|
|
47
|
+
export declare function clearConfigCache(): Promise<void>;
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/index.d.ts
|
|
50
|
+
/**
|
|
51
|
+
* The options of `format`: the options that `resolveConfig` returns, and the
|
|
52
|
+
* path of the file, whose ending says whether the text is a diagram or
|
|
53
|
+
* Markdown.
|
|
54
|
+
*/
|
|
55
|
+
export interface FormatOptions extends ReadOptions {
|
|
56
|
+
readonly filepath: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Formats a text as Prettier's `format` does, except that in Markdown only the
|
|
60
|
+
* Mermaid code blocks are formatted and the rest stays as written. Like
|
|
61
|
+
* Prettier's `format`, it reads no file and no configuration: the caller
|
|
62
|
+
* passes what `resolveConfig` returns for `filepath`, with `printWidth` in
|
|
63
|
+
* Markdown the width of the lines of the file (README, Code blocks in detail).
|
|
64
|
+
* The line endings and a byte order mark stay as they are in the text. A `.mmd`
|
|
65
|
+
* or `.mermaid` diagram that mermaid.js rejects throws the `SyntaxError` that
|
|
66
|
+
* Prettier throws for it with prettier-plugin-mmd; in Markdown, a rejected
|
|
67
|
+
* diagram stays as written, as Prettier leaves an embedded code block that it
|
|
68
|
+
* cannot format. No path is skipped (README, From a script).
|
|
69
|
+
*/
|
|
70
|
+
export declare function format(source: string, options: FormatOptions): Promise<string>;
|
|
71
|
+
//#endregion
|
|
72
|
+
export type { ReadOptions };
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { a as kindOf, c as formatMarkdown, n as endingsInWords, o as resolveConfig, r as formatDiagramSource, s as readOptions, t as clearConfigCache } from "./format-file-DSbEElSV.mjs";
|
|
2
|
+
//#region src/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* Formats a text as Prettier's `format` does, except that in Markdown only the
|
|
5
|
+
* Mermaid code blocks are formatted and the rest stays as written. Like
|
|
6
|
+
* Prettier's `format`, it reads no file and no configuration: the caller
|
|
7
|
+
* passes what `resolveConfig` returns for `filepath`, with `printWidth` in
|
|
8
|
+
* Markdown the width of the lines of the file (README, Code blocks in detail).
|
|
9
|
+
* The line endings and a byte order mark stay as they are in the text. A `.mmd`
|
|
10
|
+
* or `.mermaid` diagram that mermaid.js rejects throws the `SyntaxError` that
|
|
11
|
+
* Prettier throws for it with prettier-plugin-mmd; in Markdown, a rejected
|
|
12
|
+
* diagram stays as written, as Prettier leaves an embedded code block that it
|
|
13
|
+
* cannot format. No path is skipped (README, From a script).
|
|
14
|
+
*/
|
|
15
|
+
async function format(source, options) {
|
|
16
|
+
const kind = kindOf(options.filepath);
|
|
17
|
+
if (kind === void 0) throw new Error(`the path does not end in ${endingsInWords}: ${JSON.stringify(options.filepath)}`);
|
|
18
|
+
const read = readOptions({ ...options });
|
|
19
|
+
return kind === "diagram" ? formatDiagramSource(source, read) : (await formatMarkdown(source, read, "output")).text;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { clearConfigCache, format, resolveConfig };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mmdfmt",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Formatter for Mermaid diagrams in .mmd files and in Markdown code blocks, which changes nothing else, as a command and as a function",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"mmdfmt": "./lib/cli.mjs"
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/index.d.mts",
|
|
22
|
+
"default": "./lib/index.mjs"
|
|
23
|
+
},
|
|
20
24
|
"./package.json": "./package.json"
|
|
21
25
|
},
|
|
22
26
|
"files": [
|
|
@@ -33,10 +37,9 @@
|
|
|
33
37
|
"cli"
|
|
34
38
|
],
|
|
35
39
|
"dependencies": {
|
|
36
|
-
"@mmd-toolkit/parser": "^0.3.1",
|
|
37
40
|
"gunshi": "^0.37.3",
|
|
38
41
|
"prettier": "^3.9.0",
|
|
39
|
-
"prettier-plugin-mmd": "^0.
|
|
42
|
+
"prettier-plugin-mmd": "^0.7.0",
|
|
40
43
|
"tinyglobby": "^0.2.17"
|
|
41
44
|
},
|
|
42
45
|
"publishConfig": {
|