@writechoice/mint-cli 0.0.10 → 0.0.11
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 +42 -2
- package/bin/cli.js +14 -0
- package/package.json +1 -1
- package/src/commands/fix/parse.js +447 -0
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ writechoice check links docs.example.com http://localhost:3000
|
|
|
32
32
|
# Fix broken anchor links
|
|
33
33
|
writechoice fix links
|
|
34
34
|
|
|
35
|
+
# Fix MDX parsing errors (void tags, stray angle brackets)
|
|
36
|
+
writechoice fix parse
|
|
37
|
+
|
|
35
38
|
# Generate config.json template
|
|
36
39
|
writechoice config
|
|
37
40
|
```
|
|
@@ -94,6 +97,29 @@ writechoice fix links -r custom_report.json # Use custom report
|
|
|
94
97
|
|
|
95
98
|
**Note:** Requires JSON report from `check links` command.
|
|
96
99
|
|
|
100
|
+
### `fix parse`
|
|
101
|
+
|
|
102
|
+
Automatically fixes common MDX parsing errors: void HTML tags not self-closed and stray angle brackets in text.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
writechoice fix parse # Fix from check parse report
|
|
106
|
+
writechoice fix parse -f file.mdx # Fix a single file directly
|
|
107
|
+
writechoice fix parse -d docs # Fix files in a directory
|
|
108
|
+
writechoice fix parse -r custom_report.json # Use custom report
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Common options:**
|
|
112
|
+
- `-r, --report <path>` - Path to JSON report (default: `mdx_errors_report.json`)
|
|
113
|
+
- `-f, --file <path>` - Fix a single MDX file directly
|
|
114
|
+
- `-d, --dir <path>` - Fix MDX files in a directory
|
|
115
|
+
- `--quiet` - Suppress output
|
|
116
|
+
|
|
117
|
+
**What it fixes:**
|
|
118
|
+
- Void tags: `<br>` → `<br />`, `<img src="x">` → `<img src="x" />`
|
|
119
|
+
- Stray brackets: `x < 10` → `x < 10`, `y > 5` → `y > 5`
|
|
120
|
+
|
|
121
|
+
Content inside code blocks and inline code is never modified.
|
|
122
|
+
|
|
97
123
|
### `update`
|
|
98
124
|
|
|
99
125
|
Update CLI to latest version.
|
|
@@ -107,7 +133,8 @@ writechoice update
|
|
|
107
133
|
- **MDX Parsing Validation** - Catch syntax errors before deployment
|
|
108
134
|
- **Link Validation** - Test links against live websites with Playwright
|
|
109
135
|
- **Two-Step Anchor Validation** - Compare production vs development anchors
|
|
110
|
-
- **Auto-Fix** -
|
|
136
|
+
- **Auto-Fix Links** - Automatically correct broken anchor links
|
|
137
|
+
- **Auto-Fix Parsing** - Automatically fix void tags and stray angle brackets
|
|
111
138
|
- **Dual Report Formats** - Generates both JSON (for automation) and Markdown (for humans)
|
|
112
139
|
- **Configuration File** - Optional config.json for default settings
|
|
113
140
|
- **CI/CD Ready** - Exit codes for pipeline integration
|
|
@@ -176,10 +203,21 @@ writechoice fix links
|
|
|
176
203
|
# Fix from custom report
|
|
177
204
|
writechoice fix links -r custom_report.json
|
|
178
205
|
|
|
206
|
+
# Fix MDX parsing errors
|
|
207
|
+
writechoice fix parse
|
|
208
|
+
|
|
209
|
+
# Fix a single file directly
|
|
210
|
+
writechoice fix parse -f docs/getting-started.mdx
|
|
211
|
+
|
|
179
212
|
# Full workflow: validate -> fix -> re-validate
|
|
180
213
|
writechoice check links docs.example.com
|
|
181
214
|
writechoice fix links
|
|
182
215
|
writechoice check links docs.example.com
|
|
216
|
+
|
|
217
|
+
# Full parse workflow: validate -> fix -> re-validate
|
|
218
|
+
writechoice check parse
|
|
219
|
+
writechoice fix parse
|
|
220
|
+
writechoice check parse
|
|
183
221
|
```
|
|
184
222
|
|
|
185
223
|
## Documentation
|
|
@@ -191,6 +229,7 @@ Detailed documentation is available in the [docs/](docs/) folder:
|
|
|
191
229
|
- [check links](docs/commands/check-links.md) - Link validation
|
|
192
230
|
- [check parse](docs/commands/check-parse.md) - MDX parsing validation
|
|
193
231
|
- [fix links](docs/commands/fix-links.md) - Auto-fix broken links
|
|
232
|
+
- [fix parse](docs/commands/fix-parse.md) - Auto-fix MDX parsing errors
|
|
194
233
|
- [update](docs/commands/update.md) - Update command
|
|
195
234
|
- **Guides**
|
|
196
235
|
- [Configuration File](docs/config-file.md) - Using config.json
|
|
@@ -222,7 +261,8 @@ writechoice-mint-cli/
|
|
|
222
261
|
│ │ │ ├── links.js # Link validation
|
|
223
262
|
│ │ │ └── mdx.js # MDX parsing validation
|
|
224
263
|
│ │ └── fix/
|
|
225
|
-
│ │
|
|
264
|
+
│ │ ├── links.js # Link fixing
|
|
265
|
+
│ │ └── parse.js # Parse error fixing
|
|
226
266
|
│ └── utils/
|
|
227
267
|
│ ├── helpers.js # Utility functions
|
|
228
268
|
│ └── reports.js # Report generation
|
package/bin/cli.js
CHANGED
|
@@ -95,6 +95,20 @@ fix
|
|
|
95
95
|
await fixLinks(options);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
// Fix parse subcommand
|
|
99
|
+
fix
|
|
100
|
+
.command("parse")
|
|
101
|
+
.description("Fix common MDX parsing errors (void tags, stray angle brackets)")
|
|
102
|
+
.option("-r, --report <path>", "Path to parse validation report", "mdx_errors_report.json")
|
|
103
|
+
.option("-f, --file <path>", "Fix a single MDX file directly")
|
|
104
|
+
.option("-d, --dir <path>", "Fix MDX files in a specific directory")
|
|
105
|
+
.option("--quiet", "Suppress terminal output")
|
|
106
|
+
.action(async (options) => {
|
|
107
|
+
const { fixParse } = await import("../src/commands/fix/parse.js");
|
|
108
|
+
options.verbose = !options.quiet;
|
|
109
|
+
await fixParse(options);
|
|
110
|
+
});
|
|
111
|
+
|
|
98
112
|
// Config command
|
|
99
113
|
program
|
|
100
114
|
.command("config")
|
package/package.json
CHANGED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MDX Parse Fix Tool
|
|
3
|
+
*
|
|
4
|
+
* Fixes common MDX parsing errors in documentation files:
|
|
5
|
+
* 1. Void HTML tags not self-closed (<br> → <br />)
|
|
6
|
+
* 2. Stray < / > in text (escape to < / >)
|
|
7
|
+
*
|
|
8
|
+
* Skips content inside code fences and inline code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
|
|
12
|
+
import { join, relative, resolve } from "path";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
|
|
15
|
+
// Void HTML elements that must be self-closing in JSX/MDX
|
|
16
|
+
const VOID_ELEMENTS = [
|
|
17
|
+
"area", "base", "br", "col", "embed", "hr", "img",
|
|
18
|
+
"input", "link", "meta", "source", "track", "wbr",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const VOID_PATTERN = new RegExp(
|
|
22
|
+
`<(${VOID_ELEMENTS.join("|")})(\\s[^>]*?)?\\s*(?<!\\/)>`,
|
|
23
|
+
"gi"
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const EXCLUDED_DIRS = ["snippets", "node_modules", ".git"];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Finds MDX files to process
|
|
30
|
+
*/
|
|
31
|
+
function findMdxFiles(repoRoot, directory = null, file = null) {
|
|
32
|
+
if (file) {
|
|
33
|
+
const fullPath = resolve(repoRoot, file);
|
|
34
|
+
return existsSync(fullPath) ? [fullPath] : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const searchDirs = directory
|
|
38
|
+
? [resolve(repoRoot, directory)]
|
|
39
|
+
: [repoRoot];
|
|
40
|
+
|
|
41
|
+
const mdxFiles = [];
|
|
42
|
+
|
|
43
|
+
function walkDirectory(dir) {
|
|
44
|
+
const dirName = dir.split("/").pop();
|
|
45
|
+
if (EXCLUDED_DIRS.includes(dirName)) return;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const entries = readdirSync(dir);
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const fullPath = join(dir, entry);
|
|
51
|
+
const stat = statSync(fullPath);
|
|
52
|
+
if (stat.isDirectory()) {
|
|
53
|
+
walkDirectory(fullPath);
|
|
54
|
+
} else if (stat.isFile() && entry.endsWith(".mdx")) {
|
|
55
|
+
mdxFiles.push(fullPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`Error reading directory ${dir}: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const dir of searchDirs) {
|
|
64
|
+
if (existsSync(dir)) walkDirectory(dir);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return mdxFiles.sort();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Gets file list from a parse report (only files with errors)
|
|
72
|
+
*/
|
|
73
|
+
function getFilesFromReport(reportPath, repoRoot) {
|
|
74
|
+
if (!existsSync(reportPath)) return null;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const report = JSON.parse(readFileSync(reportPath, "utf-8"));
|
|
78
|
+
const errorFiles = (report.errors || []).map((e) =>
|
|
79
|
+
resolve(repoRoot, e.filePath)
|
|
80
|
+
);
|
|
81
|
+
return errorFiles;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(`Error reading report: ${error.message}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Splits file content into protected (code) and unprotected (text) segments.
|
|
90
|
+
* Returns an array of { text, protected } objects.
|
|
91
|
+
*/
|
|
92
|
+
function segmentContent(content) {
|
|
93
|
+
const segments = [];
|
|
94
|
+
let pos = 0;
|
|
95
|
+
const len = content.length;
|
|
96
|
+
|
|
97
|
+
// State tracking for fenced code blocks
|
|
98
|
+
let inFence = false;
|
|
99
|
+
let fenceMarker = "";
|
|
100
|
+
|
|
101
|
+
const lines = content.split("\n");
|
|
102
|
+
let lineStart = 0;
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < lines.length; i++) {
|
|
105
|
+
const line = lines[i];
|
|
106
|
+
const lineEnd = lineStart + line.length;
|
|
107
|
+
const trimmed = line.trimStart();
|
|
108
|
+
|
|
109
|
+
// Check for fenced code block boundaries
|
|
110
|
+
if (!inFence) {
|
|
111
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
112
|
+
if (fenceMatch) {
|
|
113
|
+
// Push any text before this fence line
|
|
114
|
+
if (lineStart > pos) {
|
|
115
|
+
segments.push({ text: content.slice(pos, lineStart), protected: false });
|
|
116
|
+
}
|
|
117
|
+
inFence = true;
|
|
118
|
+
fenceMarker = fenceMatch[1][0].repeat(fenceMatch[1].length);
|
|
119
|
+
// This line is protected
|
|
120
|
+
segments.push({ text: content.slice(lineStart, lineEnd), protected: true });
|
|
121
|
+
pos = lineEnd;
|
|
122
|
+
lineStart = lineEnd + 1; // +1 for newline
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Check for closing fence
|
|
127
|
+
const closeMatch = trimmed.match(/^(`{3,}|~{3,})\s*$/);
|
|
128
|
+
if (closeMatch && closeMatch[1][0] === fenceMarker[0] && closeMatch[1].length >= fenceMarker.length) {
|
|
129
|
+
// Include this line as protected, then exit fence
|
|
130
|
+
segments.push({ text: content.slice(pos, lineEnd), protected: true });
|
|
131
|
+
pos = lineEnd;
|
|
132
|
+
inFence = false;
|
|
133
|
+
fenceMarker = "";
|
|
134
|
+
lineStart = lineEnd + 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Still inside fence, continue
|
|
138
|
+
lineStart = lineEnd + 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
lineStart = lineEnd + 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Push remaining content
|
|
146
|
+
if (pos < content.length) {
|
|
147
|
+
segments.push({ text: content.slice(pos), protected: inFence });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return segments;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Fixes void HTML tags in a text segment (not inside inline code).
|
|
155
|
+
* Returns { text, count }.
|
|
156
|
+
*/
|
|
157
|
+
function fixVoidTags(text) {
|
|
158
|
+
let count = 0;
|
|
159
|
+
|
|
160
|
+
// Process the text but protect inline code spans
|
|
161
|
+
const parts = [];
|
|
162
|
+
let lastIndex = 0;
|
|
163
|
+
|
|
164
|
+
// Match inline code: `...`
|
|
165
|
+
const inlineCodeRegex = /`[^`]+`/g;
|
|
166
|
+
let match;
|
|
167
|
+
|
|
168
|
+
while ((match = inlineCodeRegex.exec(text)) !== null) {
|
|
169
|
+
// Process text before this inline code
|
|
170
|
+
const before = text.slice(lastIndex, match.index);
|
|
171
|
+
const { text: fixed, count: c } = replaceVoidTags(before);
|
|
172
|
+
parts.push(fixed);
|
|
173
|
+
count += c;
|
|
174
|
+
|
|
175
|
+
// Keep inline code unchanged
|
|
176
|
+
parts.push(match[0]);
|
|
177
|
+
lastIndex = match.index + match[0].length;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Process remaining text after last inline code
|
|
181
|
+
const remaining = text.slice(lastIndex);
|
|
182
|
+
const { text: fixed, count: c } = replaceVoidTags(remaining);
|
|
183
|
+
parts.push(fixed);
|
|
184
|
+
count += c;
|
|
185
|
+
|
|
186
|
+
return { text: parts.join(""), count };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Replaces non-self-closed void tags in a string
|
|
191
|
+
*/
|
|
192
|
+
function replaceVoidTags(text) {
|
|
193
|
+
let count = 0;
|
|
194
|
+
const result = text.replace(VOID_PATTERN, (match, tag, attrs) => {
|
|
195
|
+
// Already self-closing check (belt and suspenders)
|
|
196
|
+
if (match.trimEnd().endsWith("/>")) return match;
|
|
197
|
+
count++;
|
|
198
|
+
const attrStr = attrs ? attrs.trimEnd() : "";
|
|
199
|
+
return `<${tag}${attrStr} />`;
|
|
200
|
+
});
|
|
201
|
+
return { text: result, count };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Fixes stray < and > in a text segment (not inside inline code or tags).
|
|
206
|
+
* Returns { text, count }.
|
|
207
|
+
*/
|
|
208
|
+
function fixStrayAngleBrackets(text) {
|
|
209
|
+
let count = 0;
|
|
210
|
+
|
|
211
|
+
// Process the text but protect inline code spans and valid tags
|
|
212
|
+
const parts = [];
|
|
213
|
+
let lastIndex = 0;
|
|
214
|
+
|
|
215
|
+
// Match inline code or valid HTML/JSX tags (opening, closing, self-closing, comments)
|
|
216
|
+
const protectedRegex = /`[^`]+`|<\/[a-zA-Z][a-zA-Z0-9]*\s*>|<[a-zA-Z][a-zA-Z0-9]*(?:\s[^>]*)?\s*\/?>|<!--[\s\S]*?-->|<![^>]*>/g;
|
|
217
|
+
let match;
|
|
218
|
+
|
|
219
|
+
while ((match = protectedRegex.exec(text)) !== null) {
|
|
220
|
+
// Process text before this protected span
|
|
221
|
+
const before = text.slice(lastIndex, match.index);
|
|
222
|
+
const { text: fixed, count: c } = escapeStrayBrackets(before);
|
|
223
|
+
parts.push(fixed);
|
|
224
|
+
count += c;
|
|
225
|
+
|
|
226
|
+
// Keep protected span unchanged
|
|
227
|
+
parts.push(match[0]);
|
|
228
|
+
lastIndex = match.index + match[0].length;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Process remaining text
|
|
232
|
+
const remaining = text.slice(lastIndex);
|
|
233
|
+
const { text: fixed, count: c } = escapeStrayBrackets(remaining);
|
|
234
|
+
parts.push(fixed);
|
|
235
|
+
count += c;
|
|
236
|
+
|
|
237
|
+
return { text: parts.join(""), count };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Escapes stray < and > in plain text (no tags or code present)
|
|
242
|
+
*/
|
|
243
|
+
function escapeStrayBrackets(text) {
|
|
244
|
+
let count = 0;
|
|
245
|
+
|
|
246
|
+
// Also protect MDX expressions {}, JSX attribute patterns, and frontmatter
|
|
247
|
+
// Escape < that is NOT the start of a valid tag
|
|
248
|
+
let result = text.replace(/</g, (match, offset) => {
|
|
249
|
+
const after = text.slice(offset + 1);
|
|
250
|
+
// Valid tag starts: letter, /, !
|
|
251
|
+
if (/^[a-zA-Z\/!]/.test(after)) return match;
|
|
252
|
+
count++;
|
|
253
|
+
return "<";
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Escape > that is NOT part of a blockquote or tag end
|
|
257
|
+
// Only escape > that appears to be in running text (preceded by space/word char)
|
|
258
|
+
const srcText = result;
|
|
259
|
+
let countGt = 0;
|
|
260
|
+
result = result.replace(/>/g, (match, offset) => {
|
|
261
|
+
// Keep > at start of line (blockquote syntax)
|
|
262
|
+
const lineStart = srcText.lastIndexOf("\n", offset - 1) + 1;
|
|
263
|
+
const beforeOnLine = srcText.slice(lineStart, offset).trimStart();
|
|
264
|
+
if (beforeOnLine === "" || /^>+$/.test(beforeOnLine)) return match;
|
|
265
|
+
|
|
266
|
+
// Keep > that looks like it closes a tag (preceded by tag-like content)
|
|
267
|
+
// This shouldn't happen since valid tags are protected above, but be safe
|
|
268
|
+
const before = srcText.slice(Math.max(0, offset - 1), offset);
|
|
269
|
+
if (/[a-zA-Z0-9"'\/\-]/.test(before)) {
|
|
270
|
+
// Could be end of tag — but tags should already be protected.
|
|
271
|
+
// In plain text, this is likely stray (e.g., "a > b")
|
|
272
|
+
// Only escape if it looks like a comparison/text context
|
|
273
|
+
const surroundBefore = srcText.slice(Math.max(0, offset - 2), offset);
|
|
274
|
+
const afterChar = srcText[offset + 1] || "";
|
|
275
|
+
if (/\s/.test(surroundBefore[0]) && /[\s\w]/.test(afterChar)) {
|
|
276
|
+
countGt++;
|
|
277
|
+
return ">";
|
|
278
|
+
}
|
|
279
|
+
return match;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
countGt++;
|
|
283
|
+
return ">";
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return { text: result, count: count + countGt };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Applies all fixes to a single file
|
|
291
|
+
* Returns { voidTagFixes, strayBracketFixes }
|
|
292
|
+
*/
|
|
293
|
+
function fixFile(filePath) {
|
|
294
|
+
const content = readFileSync(filePath, "utf-8");
|
|
295
|
+
const segments = segmentContent(content);
|
|
296
|
+
|
|
297
|
+
let totalVoidFixes = 0;
|
|
298
|
+
let totalBracketFixes = 0;
|
|
299
|
+
|
|
300
|
+
const fixedSegments = segments.map((seg) => {
|
|
301
|
+
if (seg.protected) return seg.text;
|
|
302
|
+
|
|
303
|
+
// Apply void tag fixes first
|
|
304
|
+
const { text: afterVoid, count: voidCount } = fixVoidTags(seg.text);
|
|
305
|
+
totalVoidFixes += voidCount;
|
|
306
|
+
|
|
307
|
+
// Then apply stray bracket fixes
|
|
308
|
+
const { text: afterBrackets, count: bracketCount } = fixStrayAngleBrackets(afterVoid);
|
|
309
|
+
totalBracketFixes += bracketCount;
|
|
310
|
+
|
|
311
|
+
return afterBrackets;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const fixedContent = fixedSegments.join("");
|
|
315
|
+
|
|
316
|
+
if (fixedContent !== content) {
|
|
317
|
+
writeFileSync(filePath, fixedContent, "utf-8");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { voidTagFixes: totalVoidFixes, strayBracketFixes: totalBracketFixes };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Main CLI function for fixing parse errors
|
|
325
|
+
*/
|
|
326
|
+
export async function fixParse(options) {
|
|
327
|
+
const repoRoot = process.cwd();
|
|
328
|
+
|
|
329
|
+
if (!options.quiet) {
|
|
330
|
+
console.log(chalk.bold("\n\uD83D\uDD27 MDX Parse Fixer\n"));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Determine which files to fix
|
|
334
|
+
let files;
|
|
335
|
+
|
|
336
|
+
if (options.file || options.dir) {
|
|
337
|
+
// Direct file/dir mode — no report needed
|
|
338
|
+
files = findMdxFiles(repoRoot, options.dir, options.file);
|
|
339
|
+
|
|
340
|
+
if (files.length === 0) {
|
|
341
|
+
console.error("No MDX files found.");
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!options.quiet) {
|
|
346
|
+
console.log(`Found ${files.length} MDX file(s) to process\n`);
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
// Report mode
|
|
350
|
+
const reportPath = options.report || "mdx_errors_report.json";
|
|
351
|
+
|
|
352
|
+
if (!existsSync(reportPath)) {
|
|
353
|
+
console.error(chalk.red(`\n\u2717 Error: Report file not found: ${reportPath}`));
|
|
354
|
+
|
|
355
|
+
if (reportPath.endsWith(".md")) {
|
|
356
|
+
const jsonPath = reportPath.replace(/\.md$/, ".json");
|
|
357
|
+
console.error(chalk.yellow(`\n\u26A0\uFE0F The fix command requires a JSON report file.`));
|
|
358
|
+
console.error(chalk.yellow(`Try using: ${chalk.cyan(jsonPath)}`));
|
|
359
|
+
} else {
|
|
360
|
+
console.error(chalk.yellow(`\n\u26A0\uFE0F Make sure to run the validation command first:`));
|
|
361
|
+
console.error(chalk.gray(` writechoice check parse`));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!reportPath.endsWith(".json")) {
|
|
368
|
+
console.error(chalk.red(`\n\u2717 Error: The fix command requires a JSON report file.`));
|
|
369
|
+
console.error(chalk.yellow(`\nProvided file: ${reportPath}`));
|
|
370
|
+
|
|
371
|
+
if (reportPath.endsWith(".md")) {
|
|
372
|
+
const jsonPath = reportPath.replace(/\.md$/, ".json");
|
|
373
|
+
console.error(chalk.yellow(`\nThe markdown (.md) report is for human readability only.`));
|
|
374
|
+
console.error(chalk.yellow(`Please use the JSON report instead: ${chalk.cyan(jsonPath)}`));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!options.quiet) {
|
|
381
|
+
console.log(`Reading report: ${chalk.cyan(reportPath)}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
files = getFilesFromReport(reportPath, repoRoot);
|
|
385
|
+
|
|
386
|
+
if (!files || files.length === 0) {
|
|
387
|
+
if (!options.quiet) {
|
|
388
|
+
console.log(chalk.yellow("\n\u26A0\uFE0F No files with errors found in report."));
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!options.quiet) {
|
|
394
|
+
console.log(`Found ${files.length} file(s) with errors\n`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Apply fixes
|
|
399
|
+
const results = {};
|
|
400
|
+
let totalVoid = 0;
|
|
401
|
+
let totalBracket = 0;
|
|
402
|
+
|
|
403
|
+
for (const filePath of files) {
|
|
404
|
+
if (!existsSync(filePath)) {
|
|
405
|
+
if (options.verbose) {
|
|
406
|
+
console.log(`Warning: File not found: ${filePath}`);
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const { voidTagFixes, strayBracketFixes } = fixFile(filePath);
|
|
412
|
+
const totalFixes = voidTagFixes + strayBracketFixes;
|
|
413
|
+
|
|
414
|
+
if (totalFixes > 0) {
|
|
415
|
+
const relPath = relative(repoRoot, filePath);
|
|
416
|
+
results[relPath] = { voidTagFixes, strayBracketFixes };
|
|
417
|
+
totalVoid += voidTagFixes;
|
|
418
|
+
totalBracket += strayBracketFixes;
|
|
419
|
+
|
|
420
|
+
if (options.verbose) {
|
|
421
|
+
console.log(`Fixed ${chalk.cyan(relPath)}: ${voidTagFixes} void tag(s), ${strayBracketFixes} stray bracket(s)`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Summary
|
|
427
|
+
if (!options.quiet) {
|
|
428
|
+
const fileCount = Object.keys(results).length;
|
|
429
|
+
const totalFixes = totalVoid + totalBracket;
|
|
430
|
+
|
|
431
|
+
if (fileCount > 0) {
|
|
432
|
+
console.log(chalk.green(`\n\u2713 Fixed ${totalFixes} issue(s) in ${fileCount} file(s):\n`));
|
|
433
|
+
|
|
434
|
+
for (const [filePath, counts] of Object.entries(results)) {
|
|
435
|
+
const details = [];
|
|
436
|
+
if (counts.voidTagFixes > 0) details.push(`${counts.voidTagFixes} void tag(s)`);
|
|
437
|
+
if (counts.strayBracketFixes > 0) details.push(`${counts.strayBracketFixes} stray bracket(s)`);
|
|
438
|
+
console.log(` ${chalk.cyan(filePath)}: ${details.join(", ")}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
console.log(chalk.yellow("\n\u26A0\uFE0F Run validation again to verify the fixes:"));
|
|
442
|
+
console.log(chalk.gray(" writechoice check parse"));
|
|
443
|
+
} else {
|
|
444
|
+
console.log(chalk.yellow("\n\u26A0\uFE0F No fixable issues found."));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|