@takazudo/mdx-formatter 1.0.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/cli.js +92 -2
- package/dist/index.d.ts +12 -0
- package/dist/index.js +14 -1
- package/dist/rust-formatter.d.ts +19 -0
- package/dist/rust-formatter.js +17 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,37 @@ const formatted = await format('# Hello\nWorld');
|
|
|
58
58
|
console.log(formatted); // '# Hello\n\nWorld'
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### List Normalize
|
|
62
|
+
|
|
63
|
+
Five rules clean up AI-authored list-item content. They are exposed as flat
|
|
64
|
+
top-level kebab-case keys (not nested objects):
|
|
65
|
+
|
|
66
|
+
| Key | Default | Purpose |
|
|
67
|
+
| ------------------------------------- | ------------- | --------------------------------------------------------------------------------- |
|
|
68
|
+
| `tighten-list-continuations` | `"heuristic"` | Collapse blank gaps inside list items whose children are continuation paragraphs. |
|
|
69
|
+
| `tighten-list-item-spacing` | `"heuristic"` | Collapse single blank gaps between adjacent sibling list items when safe. |
|
|
70
|
+
| `recover-escaped-code-in-lists` | `"safe"` | Re-indent fenced code blocks that escaped to column 0 between list items. |
|
|
71
|
+
| `recover-escaped-tables-in-lists` | `"safe"` | Re-indent GFM tables that escaped to column 0 between list items. |
|
|
72
|
+
| `recover-escaped-paragraphs-in-lists` | `"off"` | Re-indent continuation paragraphs that escaped to column 0 (opt-in). |
|
|
73
|
+
|
|
74
|
+
Each accepts `"off"` to disable, its default middle value (`"heuristic"` or
|
|
75
|
+
`"safe"`) for the conservative trigger, or `"aggressive"` to drop the
|
|
76
|
+
structural safeguards. See the
|
|
77
|
+
[List Normalize options docs](https://takazudomodular.com/pj/mdx-formatter/docs/options/#list-normalize)
|
|
78
|
+
for per-rule before/after examples.
|
|
79
|
+
|
|
80
|
+
### Preview with `--dry-run`
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
mdx-formatter --dry-run "**/*.{md,mdx}"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Writes every rule-level change to **stderr** without touching the files.
|
|
87
|
+
Useful for auditing what the list-normalize rules (or any other rule) would
|
|
88
|
+
change before committing. Exits 0 whether or not there was anything to
|
|
89
|
+
report; conflicts with `--write` / `--check`. The same report is available
|
|
90
|
+
programmatically via the `dryRunReport()` API.
|
|
91
|
+
|
|
61
92
|
### Browser (WASM)
|
|
62
93
|
|
|
63
94
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
3
4
|
import { program } from 'commander';
|
|
4
5
|
import { glob } from 'glob';
|
|
5
6
|
import chalk from 'chalk';
|
|
6
|
-
import { formatFile, checkFile } from './index.js';
|
|
7
|
+
import { formatFile, checkFile, dryRunReport } from './index.js';
|
|
7
8
|
import { loadFullConfig } from './load-config.js';
|
|
8
9
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
9
10
|
program
|
|
@@ -13,10 +14,23 @@ program
|
|
|
13
14
|
.argument('[patterns...]', 'Glob patterns for files to format', ['**/*.{md,mdx}'])
|
|
14
15
|
.option('-w, --write', 'Write formatted files in place')
|
|
15
16
|
.option('-c, --check', 'Check if files need formatting')
|
|
17
|
+
.option('--dry-run', [
|
|
18
|
+
'Preview every change on stderr without touching files or stdout.',
|
|
19
|
+
'Each entry is: `<path>:<start>-<end> [<rule>]`, followed by indented',
|
|
20
|
+
'`- <before>` / `+ <after>` lines (≤3 each, truncated with `…`).',
|
|
21
|
+
'Exits 0 whether or not there was anything to report.',
|
|
22
|
+
].join(' '))
|
|
16
23
|
.option('--config <path>', 'Path to config file (.mdx-formatter.json)')
|
|
17
24
|
.option('--ignore <patterns>', 'Comma-separated patterns to ignore', 'node_modules/**,dist/**,build/**,.git/**,worktrees/**')
|
|
18
25
|
.action(async (patterns, options) => {
|
|
19
26
|
try {
|
|
27
|
+
// commander does not natively encode "conflicts with" rules; enforce
|
|
28
|
+
// the same constraint the Rust CLI uses so the two stay in lockstep.
|
|
29
|
+
const mutuallyExclusive = [options.write, options.check, options.dryRun].filter(Boolean).length;
|
|
30
|
+
if (mutuallyExclusive > 1) {
|
|
31
|
+
console.error(chalk.red('Error:'), '--dry-run cannot be combined with --write or --check');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
20
34
|
await main(patterns, options);
|
|
21
35
|
}
|
|
22
36
|
catch (error) {
|
|
@@ -47,7 +61,16 @@ async function main(patterns, options) {
|
|
|
47
61
|
// Remove duplicates
|
|
48
62
|
const uniqueFiles = [...new Set(files)];
|
|
49
63
|
if (uniqueFiles.length === 0) {
|
|
50
|
-
|
|
64
|
+
if (options.dryRun) {
|
|
65
|
+
console.error(chalk.yellow('No files found matching the patterns.'));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(chalk.yellow('No files found matching the patterns.'));
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (options.dryRun) {
|
|
73
|
+
await runDryRun(uniqueFiles, formatOptions);
|
|
51
74
|
return;
|
|
52
75
|
}
|
|
53
76
|
console.log(chalk.blue(`Processing ${uniqueFiles.length} file(s)...`));
|
|
@@ -126,3 +149,70 @@ async function main(patterns, options) {
|
|
|
126
149
|
process.exit(1);
|
|
127
150
|
}
|
|
128
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* `--dry-run` handler. Mirrors the Rust CLI: writes every report entry +
|
|
154
|
+
* summary to stderr, never touches stdout or disk, and always exits 0 — even
|
|
155
|
+
* when an individual file fails to parse. The report format is identical to
|
|
156
|
+
* the Rust CLI's so tooling can consume either.
|
|
157
|
+
*/
|
|
158
|
+
async function runDryRun(files, formatOptions) {
|
|
159
|
+
let totalEntries = 0;
|
|
160
|
+
let filesWithChanges = 0;
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
let content;
|
|
163
|
+
try {
|
|
164
|
+
content = await fs.readFile(file, 'utf-8');
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
168
|
+
process.stderr.write(`${file}: read error: ${message}\n`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
let entries;
|
|
172
|
+
try {
|
|
173
|
+
entries = dryRunReport(content, formatOptions);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
process.stderr.write(`${file}: parse error: ${message}\n`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (entries.length === 0)
|
|
181
|
+
continue;
|
|
182
|
+
filesWithChanges += 1;
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
totalEntries += 1;
|
|
185
|
+
writeReportEntry(file, entry);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const entryWord = totalEntries === 1 ? 'entry' : 'entries';
|
|
189
|
+
const fileWord = filesWithChanges === 1 ? 'file' : 'files';
|
|
190
|
+
process.stderr.write(`\n${totalEntries} ${entryWord} across ${filesWithChanges} ${fileWord} ` +
|
|
191
|
+
`(dry-run; no files were modified).\n`);
|
|
192
|
+
}
|
|
193
|
+
/** Render one report entry to stderr in the format shared with the Rust CLI. */
|
|
194
|
+
function writeReportEntry(path, entry) {
|
|
195
|
+
const start1 = entry.startLine + 1;
|
|
196
|
+
const end1 = entry.endLine + 1;
|
|
197
|
+
process.stderr.write(`${path}:${start1}-${end1} [${entry.rule}]\n`);
|
|
198
|
+
writeSnippet(entry.before, '-');
|
|
199
|
+
writeSnippet(entry.after, '+');
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Write up to 3 lines of a before/after snippet, truncating the rest with
|
|
203
|
+
* `…`. Empty snippets render as `(no lines)` so a delete-only change is
|
|
204
|
+
* still visible to the user.
|
|
205
|
+
*/
|
|
206
|
+
function writeSnippet(snippet, prefix) {
|
|
207
|
+
if (snippet.length === 0) {
|
|
208
|
+
process.stderr.write(` ${prefix} (no lines)\n`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const MAX = 3;
|
|
212
|
+
for (const line of snippet.slice(0, MAX)) {
|
|
213
|
+
process.stderr.write(` ${prefix} ${line}\n`);
|
|
214
|
+
}
|
|
215
|
+
if (snippet.length > MAX) {
|
|
216
|
+
process.stderr.write(` ${prefix} …\n`);
|
|
217
|
+
}
|
|
218
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* Uses the Rust napi formatter as the sole formatting engine.
|
|
4
4
|
*/
|
|
5
5
|
import { detectMdx } from './detect-mdx.js';
|
|
6
|
+
import type { DryRunReportEntry } from './rust-formatter.js';
|
|
6
7
|
import type { FormatOptions } from './types.js';
|
|
7
8
|
export { detectMdx };
|
|
9
|
+
export type { DryRunReportEntry };
|
|
8
10
|
/**
|
|
9
11
|
* Format markdown/MDX content using the Rust formatter.
|
|
10
12
|
*/
|
|
@@ -23,11 +25,21 @@ export declare function formatFile(filePath: string, options?: FormatOptions): P
|
|
|
23
25
|
* Check if a file needs formatting
|
|
24
26
|
*/
|
|
25
27
|
export declare function checkFile(filePath: string, options?: FormatOptions): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Compute the dry-run report for `content` without modifying it.
|
|
30
|
+
*
|
|
31
|
+
* Entries describe every change the formatter's rules would make, with
|
|
32
|
+
* 0-indexed line ranges and `before`/`after` snippets. Used by the CLI
|
|
33
|
+
* `--dry-run` flag and exposed here so library consumers can audit a corpus
|
|
34
|
+
* programmatically.
|
|
35
|
+
*/
|
|
36
|
+
export declare function dryRunReport(content: string, options?: FormatOptions): DryRunReportEntry[];
|
|
26
37
|
declare const _default: {
|
|
27
38
|
format: typeof format;
|
|
28
39
|
formatSync: typeof formatSync;
|
|
29
40
|
formatFile: typeof formatFile;
|
|
30
41
|
checkFile: typeof checkFile;
|
|
42
|
+
dryRunReport: typeof dryRunReport;
|
|
31
43
|
detectMdx: typeof detectMdx;
|
|
32
44
|
};
|
|
33
45
|
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { promises as fs } from 'fs';
|
|
6
6
|
import { loadConfig } from './load-config.js';
|
|
7
7
|
import { detectMdx } from './detect-mdx.js';
|
|
8
|
-
import { nativeFormat } from './rust-formatter.js';
|
|
8
|
+
import { nativeFormat, nativeDryRunReport } from './rust-formatter.js';
|
|
9
9
|
import { formatterSettings } from './settings.js';
|
|
10
10
|
export { detectMdx };
|
|
11
11
|
/**
|
|
@@ -44,10 +44,23 @@ export async function checkFile(filePath, options = {}) {
|
|
|
44
44
|
const formatted = await format(content, options);
|
|
45
45
|
return content !== formatted;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Compute the dry-run report for `content` without modifying it.
|
|
49
|
+
*
|
|
50
|
+
* Entries describe every change the formatter's rules would make, with
|
|
51
|
+
* 0-indexed line ranges and `before`/`after` snippets. Used by the CLI
|
|
52
|
+
* `--dry-run` flag and exposed here so library consumers can audit a corpus
|
|
53
|
+
* programmatically.
|
|
54
|
+
*/
|
|
55
|
+
export function dryRunReport(content, options = {}) {
|
|
56
|
+
const settings = loadConfig(options);
|
|
57
|
+
return nativeDryRunReport(content, JSON.stringify(settings));
|
|
58
|
+
}
|
|
47
59
|
export default {
|
|
48
60
|
format,
|
|
49
61
|
formatSync,
|
|
50
62
|
formatFile,
|
|
51
63
|
checkFile,
|
|
64
|
+
dryRunReport,
|
|
52
65
|
detectMdx,
|
|
53
66
|
};
|
package/dist/rust-formatter.d.ts
CHANGED
|
@@ -4,11 +4,30 @@
|
|
|
4
4
|
* This is the sole formatting engine — no TypeScript fallback.
|
|
5
5
|
*/
|
|
6
6
|
type NativeFormat = (content: string, settingsJson: string) => string;
|
|
7
|
+
/**
|
|
8
|
+
* One dry-run report entry, as produced by the Rust `ReportSink` and
|
|
9
|
+
* serialized through the napi `dry_run_report` / `dryRunReport` export.
|
|
10
|
+
* Line numbers are 0-indexed; the TS CLI converts them to 1-based when
|
|
11
|
+
* printing.
|
|
12
|
+
*/
|
|
13
|
+
export interface DryRunReportEntry {
|
|
14
|
+
rule: string;
|
|
15
|
+
startLine: number;
|
|
16
|
+
endLine: number;
|
|
17
|
+
before: string[];
|
|
18
|
+
after: string[];
|
|
19
|
+
}
|
|
7
20
|
/**
|
|
8
21
|
* The native format function. Loaded once at module init.
|
|
9
22
|
* Throws if the native module is not available.
|
|
10
23
|
*/
|
|
11
24
|
export declare const nativeFormat: NativeFormat;
|
|
25
|
+
/**
|
|
26
|
+
* Compute the dry-run report for `content`. Returns a parsed array of
|
|
27
|
+
* `DryRunReportEntry`. Throws if the binary is older than this TS build and
|
|
28
|
+
* does not yet export `dryRunReport` (see `build:rust`).
|
|
29
|
+
*/
|
|
30
|
+
export declare function nativeDryRunReport(content: string, settingsJson: string): DryRunReportEntry[];
|
|
12
31
|
/**
|
|
13
32
|
* Check if the Rust formatter is available
|
|
14
33
|
*/
|
package/dist/rust-formatter.js
CHANGED
|
@@ -28,7 +28,7 @@ function loadNativeModule() {
|
|
|
28
28
|
// the code from the current checkout instead of the last published binary.
|
|
29
29
|
try {
|
|
30
30
|
const native = require('../crates/mdx-formatter-napi/mdx-formatter-napi.node');
|
|
31
|
-
return native
|
|
31
|
+
return native;
|
|
32
32
|
}
|
|
33
33
|
catch {
|
|
34
34
|
// Local build not present, fall back to platform-specific npm package.
|
|
@@ -37,7 +37,7 @@ function loadNativeModule() {
|
|
|
37
37
|
if (packageName) {
|
|
38
38
|
try {
|
|
39
39
|
const native = require(packageName);
|
|
40
|
-
return native
|
|
40
|
+
return native;
|
|
41
41
|
}
|
|
42
42
|
catch {
|
|
43
43
|
// Platform package not installed, surface a build hint below.
|
|
@@ -45,11 +45,25 @@ function loadNativeModule() {
|
|
|
45
45
|
}
|
|
46
46
|
throw new Error('Rust native module not available. Build it with: pnpm build:rust');
|
|
47
47
|
}
|
|
48
|
+
const nativeModule = loadNativeModule();
|
|
48
49
|
/**
|
|
49
50
|
* The native format function. Loaded once at module init.
|
|
50
51
|
* Throws if the native module is not available.
|
|
51
52
|
*/
|
|
52
|
-
export const nativeFormat =
|
|
53
|
+
export const nativeFormat = nativeModule.format;
|
|
54
|
+
/**
|
|
55
|
+
* Compute the dry-run report for `content`. Returns a parsed array of
|
|
56
|
+
* `DryRunReportEntry`. Throws if the binary is older than this TS build and
|
|
57
|
+
* does not yet export `dryRunReport` (see `build:rust`).
|
|
58
|
+
*/
|
|
59
|
+
export function nativeDryRunReport(content, settingsJson) {
|
|
60
|
+
if (typeof nativeModule.dryRunReport !== 'function') {
|
|
61
|
+
throw new Error('The loaded Rust native module does not export `dryRunReport`. ' +
|
|
62
|
+
'Rebuild it with: pnpm build:rust');
|
|
63
|
+
}
|
|
64
|
+
const json = nativeModule.dryRunReport(content, settingsJson);
|
|
65
|
+
return JSON.parse(json);
|
|
66
|
+
}
|
|
53
67
|
/**
|
|
54
68
|
* Check if the Rust formatter is available
|
|
55
69
|
*/
|