@writechoice/mint-cli 0.0.9 → 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 +57 -2
- package/bin/cli.js +25 -0
- package/package.json +1 -1
- package/src/commands/config.js +70 -0
- package/src/commands/fix/parse.js +447 -0
package/README.md
CHANGED
|
@@ -31,10 +31,27 @@ writechoice check links docs.example.com http://localhost:3000
|
|
|
31
31
|
|
|
32
32
|
# Fix broken anchor links
|
|
33
33
|
writechoice fix links
|
|
34
|
+
|
|
35
|
+
# Fix MDX parsing errors (void tags, stray angle brackets)
|
|
36
|
+
writechoice fix parse
|
|
37
|
+
|
|
38
|
+
# Generate config.json template
|
|
39
|
+
writechoice config
|
|
34
40
|
```
|
|
35
41
|
|
|
36
42
|
## Commands
|
|
37
43
|
|
|
44
|
+
### `config`
|
|
45
|
+
|
|
46
|
+
Generates a config.json template file with all available options.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
writechoice config # Create config.json
|
|
50
|
+
writechoice config --force # Overwrite existing config.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Output:** Creates `config.json` in the current directory with placeholder values.
|
|
54
|
+
|
|
38
55
|
### `check parse`
|
|
39
56
|
|
|
40
57
|
Validates MDX files for parsing errors.
|
|
@@ -80,6 +97,29 @@ writechoice fix links -r custom_report.json # Use custom report
|
|
|
80
97
|
|
|
81
98
|
**Note:** Requires JSON report from `check links` command.
|
|
82
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
|
+
|
|
83
123
|
### `update`
|
|
84
124
|
|
|
85
125
|
Update CLI to latest version.
|
|
@@ -93,7 +133,8 @@ writechoice update
|
|
|
93
133
|
- **MDX Parsing Validation** - Catch syntax errors before deployment
|
|
94
134
|
- **Link Validation** - Test links against live websites with Playwright
|
|
95
135
|
- **Two-Step Anchor Validation** - Compare production vs development anchors
|
|
96
|
-
- **Auto-Fix** -
|
|
136
|
+
- **Auto-Fix Links** - Automatically correct broken anchor links
|
|
137
|
+
- **Auto-Fix Parsing** - Automatically fix void tags and stray angle brackets
|
|
97
138
|
- **Dual Report Formats** - Generates both JSON (for automation) and Markdown (for humans)
|
|
98
139
|
- **Configuration File** - Optional config.json for default settings
|
|
99
140
|
- **CI/CD Ready** - Exit codes for pipeline integration
|
|
@@ -162,10 +203,21 @@ writechoice fix links
|
|
|
162
203
|
# Fix from custom report
|
|
163
204
|
writechoice fix links -r custom_report.json
|
|
164
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
|
+
|
|
165
212
|
# Full workflow: validate -> fix -> re-validate
|
|
166
213
|
writechoice check links docs.example.com
|
|
167
214
|
writechoice fix links
|
|
168
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
|
|
169
221
|
```
|
|
170
222
|
|
|
171
223
|
## Documentation
|
|
@@ -173,9 +225,11 @@ writechoice check links docs.example.com
|
|
|
173
225
|
Detailed documentation is available in the [docs/](docs/) folder:
|
|
174
226
|
|
|
175
227
|
- **Commands**
|
|
228
|
+
- [config](docs/commands/config.md) - Generate config.json template
|
|
176
229
|
- [check links](docs/commands/check-links.md) - Link validation
|
|
177
230
|
- [check parse](docs/commands/check-parse.md) - MDX parsing validation
|
|
178
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
|
|
179
233
|
- [update](docs/commands/update.md) - Update command
|
|
180
234
|
- **Guides**
|
|
181
235
|
- [Configuration File](docs/config-file.md) - Using config.json
|
|
@@ -207,7 +261,8 @@ writechoice-mint-cli/
|
|
|
207
261
|
│ │ │ ├── links.js # Link validation
|
|
208
262
|
│ │ │ └── mdx.js # MDX parsing validation
|
|
209
263
|
│ │ └── fix/
|
|
210
|
-
│ │
|
|
264
|
+
│ │ ├── links.js # Link fixing
|
|
265
|
+
│ │ └── parse.js # Parse error fixing
|
|
211
266
|
│ └── utils/
|
|
212
267
|
│ ├── helpers.js # Utility functions
|
|
213
268
|
│ └── reports.js # Report generation
|
package/bin/cli.js
CHANGED
|
@@ -95,6 +95,31 @@ 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
|
+
|
|
112
|
+
// Config command
|
|
113
|
+
program
|
|
114
|
+
.command("config")
|
|
115
|
+
.description("Generate a config.json template file")
|
|
116
|
+
.option("--force", "Overwrite existing config.json file")
|
|
117
|
+
.option("--quiet", "Suppress terminal output")
|
|
118
|
+
.action(async (options) => {
|
|
119
|
+
const { generateConfig } = await import("../src/commands/config.js");
|
|
120
|
+
await generateConfig(options);
|
|
121
|
+
});
|
|
122
|
+
|
|
98
123
|
// Update command
|
|
99
124
|
program
|
|
100
125
|
.command("update")
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config File Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates a config.json template file with all available options.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeFileSync, existsSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates a config.json template file
|
|
13
|
+
* @param {Object} options - CLI options
|
|
14
|
+
*/
|
|
15
|
+
export async function generateConfig(options) {
|
|
16
|
+
const configPath = join(process.cwd(), "config.json");
|
|
17
|
+
|
|
18
|
+
// Check if config.json already exists
|
|
19
|
+
if (existsSync(configPath) && !options.force) {
|
|
20
|
+
console.error(chalk.red("\n✗ Error: config.json already exists in the current directory."));
|
|
21
|
+
console.log(chalk.yellow("\nUse --force to overwrite the existing file:"));
|
|
22
|
+
console.log(chalk.gray(" writechoice config --force"));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Create the config template
|
|
27
|
+
const configTemplate = {
|
|
28
|
+
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
29
|
+
"description": "Configuration file for WriteChoice Mint CLI",
|
|
30
|
+
|
|
31
|
+
"source": "https://docs.example.com",
|
|
32
|
+
"target": "http://localhost:3000",
|
|
33
|
+
|
|
34
|
+
"links": {
|
|
35
|
+
"file": null,
|
|
36
|
+
"dir": null,
|
|
37
|
+
"output": "links_report",
|
|
38
|
+
"dry-run": false,
|
|
39
|
+
"quiet": false,
|
|
40
|
+
"concurrency": 25,
|
|
41
|
+
"headless": true
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"parse": {
|
|
45
|
+
"file": null,
|
|
46
|
+
"dir": null,
|
|
47
|
+
"quiet": false
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
writeFileSync(configPath, JSON.stringify(configTemplate, null, 2), "utf-8");
|
|
53
|
+
|
|
54
|
+
if (!options.quiet) {
|
|
55
|
+
console.log(chalk.green("\n✓ Successfully created config.json\n"));
|
|
56
|
+
console.log(chalk.bold("Next steps:\n"));
|
|
57
|
+
console.log("1. Edit config.json and update the placeholder values:");
|
|
58
|
+
console.log(chalk.cyan(" - source:") + " Your production documentation URL");
|
|
59
|
+
console.log(chalk.cyan(" - target:") + " Your validation environment URL (e.g., localhost:3000)");
|
|
60
|
+
console.log("\n2. Run validation commands without arguments:");
|
|
61
|
+
console.log(chalk.gray(" writechoice check links"));
|
|
62
|
+
console.log(chalk.gray(" writechoice check parse"));
|
|
63
|
+
console.log("\n3. For more details, see:");
|
|
64
|
+
console.log(chalk.gray(" docs/config-file.md"));
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(chalk.red(`\n✗ Error creating config.json: ${error.message}`));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -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
|
+
}
|