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.
@@ -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 };
@@ -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.1.0",
4
- "description": "Command-line formatter for Mermaid diagrams in .mmd files and in Markdown code blocks, which changes nothing else",
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.6.0",
42
+ "prettier-plugin-mmd": "^0.7.0",
40
43
  "tinyglobby": "^0.2.17"
41
44
  },
42
45
  "publishConfig": {