@writechoice/mint-cli 0.0.13 → 0.0.15
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/bin/cli.js +42 -0
- package/package.json +2 -1
- package/src/commands/fix/codeblocks.js +252 -0
- package/src/commands/fix/images.js +220 -0
- package/src/utils/config.js +63 -0
package/bin/cli.js
CHANGED
|
@@ -111,6 +111,48 @@ fix
|
|
|
111
111
|
await fixParse(options);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
// Fix codeblocks subcommand
|
|
115
|
+
fix
|
|
116
|
+
.command("codeblocks")
|
|
117
|
+
.description("Fix code block flags (expandable, lines, wrap) in MDX files")
|
|
118
|
+
.option("-f, --file <path>", "Fix a single MDX file directly")
|
|
119
|
+
.option("-d, --dir <path>", "Fix MDX files in a specific directory")
|
|
120
|
+
.option("-t, --threshold <number>", "Line count threshold for expandable (default: 15)")
|
|
121
|
+
.option("--no-expandable", "Skip expandable threshold processing")
|
|
122
|
+
.option("--lines", "Add 'lines' to all code blocks that lack it")
|
|
123
|
+
.option("--remove-lines", "Remove 'lines' from all code blocks that have it")
|
|
124
|
+
.option("--wrap", "Add 'wrap' to all code blocks that lack it")
|
|
125
|
+
.option("--remove-wrap", "Remove 'wrap' from all code blocks that have it")
|
|
126
|
+
.option("--dry-run", "Preview changes without writing files")
|
|
127
|
+
.option("--quiet", "Suppress terminal output")
|
|
128
|
+
.action(async (options) => {
|
|
129
|
+
const { loadConfig, mergeCodeblocksConfig } = await import("../src/utils/config.js");
|
|
130
|
+
const { fixCodeblocks } = await import("../src/commands/fix/codeblocks.js");
|
|
131
|
+
|
|
132
|
+
const config = loadConfig();
|
|
133
|
+
const mergedOptions = mergeCodeblocksConfig(options, config);
|
|
134
|
+
mergedOptions.verbose = !mergedOptions.quiet;
|
|
135
|
+
await fixCodeblocks(mergedOptions);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Fix images subcommand
|
|
139
|
+
fix
|
|
140
|
+
.command("images")
|
|
141
|
+
.description("Wrap standalone images in <Frame> components in MDX files")
|
|
142
|
+
.option("-f, --file <path>", "Fix a single MDX file directly")
|
|
143
|
+
.option("-d, --dir <path>", "Fix MDX files in a specific directory")
|
|
144
|
+
.option("--dry-run", "Preview changes without writing files")
|
|
145
|
+
.option("--quiet", "Suppress terminal output")
|
|
146
|
+
.action(async (options) => {
|
|
147
|
+
const { loadConfig, mergeImagesConfig } = await import("../src/utils/config.js");
|
|
148
|
+
const { fixImages } = await import("../src/commands/fix/images.js");
|
|
149
|
+
|
|
150
|
+
const config = loadConfig();
|
|
151
|
+
const mergedOptions = mergeImagesConfig(options, config);
|
|
152
|
+
mergedOptions.verbose = !mergedOptions.quiet;
|
|
153
|
+
await fixImages(mergedOptions);
|
|
154
|
+
});
|
|
155
|
+
|
|
114
156
|
// Config command
|
|
115
157
|
program
|
|
116
158
|
.command("config")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@writechoice/mint-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "CLI tool for Mintlify documentation validation and utilities",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
13
13
|
"dev": "node bin/cli.js",
|
|
14
|
+
"release": "bash publish.sh",
|
|
14
15
|
"postinstall": "echo \"\\n⚠️ Don't forget to install Playwright browsers:\\n npx playwright install chromium\\n\""
|
|
15
16
|
},
|
|
16
17
|
"keywords": [
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Block Fix Tool
|
|
3
|
+
*
|
|
4
|
+
* Fixes code block flags in MDX documentation files:
|
|
5
|
+
* - expandable: adds when line count > threshold, removes when < threshold
|
|
6
|
+
* - lines: adds to all code blocks (--lines) or removes from all (--remove-lines)
|
|
7
|
+
* - wrap: adds to all code blocks (--wrap) or removes from all (--remove-wrap)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
|
|
11
|
+
import { join, relative, resolve } from "path";
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_THRESHOLD = 15;
|
|
15
|
+
const EXCLUDED_DIRS = ["node_modules", ".git"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Finds MDX files to process
|
|
19
|
+
*/
|
|
20
|
+
function findMdxFiles(repoRoot, directory = null, file = null) {
|
|
21
|
+
if (file) {
|
|
22
|
+
const fullPath = resolve(repoRoot, file);
|
|
23
|
+
return existsSync(fullPath) ? [fullPath] : [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const searchDirs = directory ? [resolve(repoRoot, directory)] : [repoRoot];
|
|
27
|
+
const mdxFiles = [];
|
|
28
|
+
|
|
29
|
+
function walkDirectory(dir) {
|
|
30
|
+
const dirName = dir.split("/").pop();
|
|
31
|
+
if (EXCLUDED_DIRS.includes(dirName)) return;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const entries = readdirSync(dir);
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const fullPath = join(dir, entry);
|
|
37
|
+
const stat = statSync(fullPath);
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
walkDirectory(fullPath);
|
|
40
|
+
} else if (stat.isFile() && entry.endsWith(".mdx")) {
|
|
41
|
+
mdxFiles.push(fullPath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(`Error reading directory ${dir}: ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const dir of searchDirs) {
|
|
50
|
+
if (existsSync(dir)) walkDirectory(dir);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return mdxFiles.sort();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Splits content into lines preserving original line endings.
|
|
58
|
+
*/
|
|
59
|
+
function splitLines(content) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
let pos = 0;
|
|
62
|
+
while (pos < content.length) {
|
|
63
|
+
const nlPos = content.indexOf("\n", pos);
|
|
64
|
+
if (nlPos === -1) {
|
|
65
|
+
lines.push(content.slice(pos));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
lines.push(content.slice(pos, nlPos + 1));
|
|
69
|
+
pos = nlPos + 1;
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Processes the token list for a single code block's info string.
|
|
76
|
+
* Returns { newTokens, changes }.
|
|
77
|
+
*/
|
|
78
|
+
function processInfoTokens(tokens, lineCount, lineNum, options) {
|
|
79
|
+
const changes = [];
|
|
80
|
+
let newTokens = [...tokens];
|
|
81
|
+
|
|
82
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
83
|
+
|
|
84
|
+
// expandable: threshold-based (unless disabled via --no-expandable)
|
|
85
|
+
if (options.expandable !== false) {
|
|
86
|
+
const hasExpandable = newTokens.includes("expandable");
|
|
87
|
+
if (hasExpandable && lineCount < threshold) {
|
|
88
|
+
newTokens = newTokens.filter((t) => t !== "expandable");
|
|
89
|
+
changes.push(`line ${lineNum}: removed 'expandable' (${lineCount} lines < ${threshold})`);
|
|
90
|
+
} else if (!hasExpandable && lineCount > threshold) {
|
|
91
|
+
newTokens.push("expandable");
|
|
92
|
+
changes.push(`line ${lineNum}: added 'expandable' (${lineCount} lines > ${threshold})`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// lines: add or remove
|
|
97
|
+
const hasLines = newTokens.includes("lines");
|
|
98
|
+
if (options.lines && !hasLines) {
|
|
99
|
+
newTokens.push("lines");
|
|
100
|
+
changes.push(`line ${lineNum}: added 'lines'`);
|
|
101
|
+
} else if (options.removeLines && hasLines) {
|
|
102
|
+
newTokens = newTokens.filter((t) => t !== "lines");
|
|
103
|
+
changes.push(`line ${lineNum}: removed 'lines'`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// wrap: add or remove
|
|
107
|
+
const hasWrap = newTokens.includes("wrap");
|
|
108
|
+
if (options.wrap && !hasWrap) {
|
|
109
|
+
newTokens.push("wrap");
|
|
110
|
+
changes.push(`line ${lineNum}: added 'wrap'`);
|
|
111
|
+
} else if (options.removeWrap && hasWrap) {
|
|
112
|
+
newTokens = newTokens.filter((t) => t !== "wrap");
|
|
113
|
+
changes.push(`line ${lineNum}: removed 'wrap'`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { newTokens, changes };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Scans MDX content for fenced code blocks and applies flag fixes.
|
|
121
|
+
* Returns { newContent, changes }.
|
|
122
|
+
*/
|
|
123
|
+
function processContent(content, options) {
|
|
124
|
+
const lines = splitLines(content);
|
|
125
|
+
const result = [];
|
|
126
|
+
const changes = [];
|
|
127
|
+
let i = 0;
|
|
128
|
+
|
|
129
|
+
while (i < lines.length) {
|
|
130
|
+
const line = lines[i];
|
|
131
|
+
// Strip trailing newline for matching, preserving it for output
|
|
132
|
+
const stripped = line.replace(/\r?\n$/, "");
|
|
133
|
+
|
|
134
|
+
// Detect a fenced code block opener: optional indent + 3+ backticks + info string
|
|
135
|
+
const openMatch = stripped.match(/^([ \t]*)(`{3,})(.*)$/);
|
|
136
|
+
|
|
137
|
+
if (openMatch) {
|
|
138
|
+
const indent = openMatch[1];
|
|
139
|
+
const fence = openMatch[2];
|
|
140
|
+
const info = openMatch[3];
|
|
141
|
+
const fenceLen = fence.length;
|
|
142
|
+
|
|
143
|
+
// Find the matching closing fence
|
|
144
|
+
let j = i + 1;
|
|
145
|
+
while (j < lines.length) {
|
|
146
|
+
const closeStripped = lines[j].replace(/\r?\n$/, "");
|
|
147
|
+
const closeMatch = closeStripped.match(/^[ \t]*(`{3,})[ \t]*$/);
|
|
148
|
+
if (closeMatch && closeMatch[1].length >= fenceLen) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
j++;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const bodyLines = lines.slice(i + 1, j);
|
|
155
|
+
const lineCount = bodyLines.length;
|
|
156
|
+
|
|
157
|
+
// Parse info string tokens
|
|
158
|
+
const infoStripped = info.trim();
|
|
159
|
+
const tokens = infoStripped ? infoStripped.split(/\s+/) : [];
|
|
160
|
+
|
|
161
|
+
const { newTokens, changes: blockChanges } = processInfoTokens(tokens, lineCount, i + 1, options);
|
|
162
|
+
changes.push(...blockChanges);
|
|
163
|
+
|
|
164
|
+
// Reconstruct the opening fence line with original line ending
|
|
165
|
+
const lineEnding = line.slice(stripped.length);
|
|
166
|
+
const newInfo = newTokens.join(" ");
|
|
167
|
+
result.push(`${indent}${fence}${newInfo}${lineEnding}`);
|
|
168
|
+
result.push(...bodyLines);
|
|
169
|
+
|
|
170
|
+
if (j < lines.length) {
|
|
171
|
+
result.push(lines[j]); // closing fence
|
|
172
|
+
i = j + 1;
|
|
173
|
+
} else {
|
|
174
|
+
// Unterminated block — leave as-is
|
|
175
|
+
i = j;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
result.push(line);
|
|
179
|
+
i++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { newContent: result.join(""), changes };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Main exported function for the fix codeblocks command.
|
|
188
|
+
*/
|
|
189
|
+
export async function fixCodeblocks(options) {
|
|
190
|
+
const repoRoot = process.cwd();
|
|
191
|
+
|
|
192
|
+
if (!options.quiet) {
|
|
193
|
+
console.log(chalk.bold("\n🔧 Code Block Fixer\n"));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const files = findMdxFiles(repoRoot, options.dir, options.file);
|
|
197
|
+
|
|
198
|
+
if (files.length === 0) {
|
|
199
|
+
console.error(chalk.red("✗ No MDX files found."));
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!options.quiet) {
|
|
204
|
+
console.log(`Found ${files.length} MDX file(s) to process\n`);
|
|
205
|
+
if (options.dryRun) {
|
|
206
|
+
console.log(chalk.yellow("Dry run — no files will be written\n"));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const results = {};
|
|
211
|
+
let totalChanges = 0;
|
|
212
|
+
|
|
213
|
+
for (const filePath of files) {
|
|
214
|
+
const content = readFileSync(filePath, "utf-8");
|
|
215
|
+
const { newContent, changes } = processContent(content, options);
|
|
216
|
+
|
|
217
|
+
if (changes.length > 0) {
|
|
218
|
+
const relPath = relative(repoRoot, filePath);
|
|
219
|
+
results[relPath] = changes;
|
|
220
|
+
totalChanges += changes.length;
|
|
221
|
+
|
|
222
|
+
if (options.verbose) {
|
|
223
|
+
console.log(`${chalk.cyan(relPath)}: ${changes.length} change(s)`);
|
|
224
|
+
for (const change of changes) {
|
|
225
|
+
console.log(` ${change}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!options.dryRun) {
|
|
230
|
+
writeFileSync(filePath, newContent, "utf-8");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Summary
|
|
236
|
+
if (!options.quiet) {
|
|
237
|
+
const fileCount = Object.keys(results).length;
|
|
238
|
+
|
|
239
|
+
if (fileCount > 0) {
|
|
240
|
+
const verb = options.dryRun ? "Would make" : "Made";
|
|
241
|
+
console.log(chalk.green(`\n✓ ${verb} ${totalChanges} change(s) in ${fileCount} file(s)`));
|
|
242
|
+
|
|
243
|
+
if (!options.verbose) {
|
|
244
|
+
for (const [filePath, changes] of Object.entries(results)) {
|
|
245
|
+
console.log(` ${chalk.cyan(filePath)}: ${changes.length} change(s)`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
console.log(chalk.yellow("⚠️ No changes needed."));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix Images Tool
|
|
3
|
+
*
|
|
4
|
+
* Wraps standalone images in MDX files with <Frame> components.
|
|
5
|
+
* Handles both Markdown images () and HTML <img> tags.
|
|
6
|
+
*
|
|
7
|
+
* Skips images that are already inside:
|
|
8
|
+
* - <Frame> blocks
|
|
9
|
+
* - Fenced code blocks
|
|
10
|
+
* - Markdown tables
|
|
11
|
+
* - HTML tables
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
|
|
15
|
+
import { join, relative, resolve } from "path";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
|
|
18
|
+
const EXCLUDED_DIRS = ["node_modules", ".git"];
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Protection patterns (tokenized so images inside them are never touched)
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
// Existing <Frame>...</Frame> blocks (case-insensitive, with optional attributes)
|
|
25
|
+
const FRAME_RE = /<Frame(?:\s[^>]*)?>[\s\S]*?<\/Frame>/gi;
|
|
26
|
+
|
|
27
|
+
// HTML tables
|
|
28
|
+
const HTML_TABLE_RE = /<table(?:\s[^>]*)?>[\s\S]*?<\/table>/gi;
|
|
29
|
+
|
|
30
|
+
// Markdown tables: one or more consecutive lines starting with |
|
|
31
|
+
const MD_TABLE_RE = /^(?:\|[^\n]*\n)+(?:\|[^\n]*)?/gm;
|
|
32
|
+
|
|
33
|
+
// Fenced code blocks (backtick or tilde, 3+)
|
|
34
|
+
const FENCE_RE = /^[ \t]*(`{3,}|~{3,})[ \t]*[^\n]*\n[\s\S]*?\n[ \t]*\1[ \t]*$/gm;
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
// Image patterns (applied after protection)
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
// Standalone Markdown image on its own line: 
|
|
41
|
+
const MD_IMAGE_RE = /^([ \t]*)(!\[[^\]\n]*\]\([^\)\n]+\))[ \t]*$/gm;
|
|
42
|
+
|
|
43
|
+
// Standalone HTML <img> tag on its own line
|
|
44
|
+
const HTML_IMG_RE = /^([ \t]*)(<img\b[^>\n]*\/?>)[ \t]*$/gm;
|
|
45
|
+
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
// Tokenizer helpers
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function tokenize(pattern, text, tag) {
|
|
51
|
+
const stash = [];
|
|
52
|
+
const result = text.replace(pattern, (match) => {
|
|
53
|
+
const idx = stash.length;
|
|
54
|
+
stash.push(match);
|
|
55
|
+
return `\x00${tag}${idx}\x00`;
|
|
56
|
+
});
|
|
57
|
+
return { text: result, stash };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function detokenize(text, tag, stash) {
|
|
61
|
+
const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
62
|
+
return text.replace(new RegExp(`\x00${escapedTag}(\\d+)\x00`, "g"), (_, idx) => {
|
|
63
|
+
return stash[parseInt(idx, 10)];
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
// Core processing
|
|
69
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Processes MDX content and wraps standalone images in <Frame> components.
|
|
73
|
+
* Returns { newContent, count }.
|
|
74
|
+
*/
|
|
75
|
+
function processContent(content) {
|
|
76
|
+
let text = content;
|
|
77
|
+
|
|
78
|
+
// 1. Protect existing <Frame> blocks
|
|
79
|
+
const frameResult = tokenize(FRAME_RE, text, "FRAME");
|
|
80
|
+
text = frameResult.text;
|
|
81
|
+
|
|
82
|
+
// 2. Protect HTML tables
|
|
83
|
+
const htmlTableResult = tokenize(HTML_TABLE_RE, text, "HTMLTABLE");
|
|
84
|
+
text = htmlTableResult.text;
|
|
85
|
+
|
|
86
|
+
// 3. Protect Markdown tables
|
|
87
|
+
const mdTableResult = tokenize(MD_TABLE_RE, text, "MDTABLE");
|
|
88
|
+
text = mdTableResult.text;
|
|
89
|
+
|
|
90
|
+
// 4. Protect fenced code blocks
|
|
91
|
+
const fenceResult = tokenize(FENCE_RE, text, "FENCE");
|
|
92
|
+
text = fenceResult.text;
|
|
93
|
+
|
|
94
|
+
// 5. Wrap standalone images
|
|
95
|
+
let count = 0;
|
|
96
|
+
|
|
97
|
+
text = text.replace(MD_IMAGE_RE, (match, indent, image) => {
|
|
98
|
+
count++;
|
|
99
|
+
return `${indent}<Frame>\n${indent}${image}\n${indent}</Frame>`;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
text = text.replace(HTML_IMG_RE, (match, indent, tag) => {
|
|
103
|
+
count++;
|
|
104
|
+
return `${indent}<Frame>\n${indent}${tag}\n${indent}</Frame>`;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// 6. Restore all protected regions (reverse order)
|
|
108
|
+
text = detokenize(text, "FENCE", fenceResult.stash);
|
|
109
|
+
text = detokenize(text, "MDTABLE", mdTableResult.stash);
|
|
110
|
+
text = detokenize(text, "HTMLTABLE", htmlTableResult.stash);
|
|
111
|
+
text = detokenize(text, "FRAME", frameResult.stash);
|
|
112
|
+
|
|
113
|
+
return { newContent: text, count };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
// File discovery
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function findMdxFiles(repoRoot, directory = null, file = null) {
|
|
121
|
+
if (file) {
|
|
122
|
+
const fullPath = resolve(repoRoot, file);
|
|
123
|
+
return existsSync(fullPath) ? [fullPath] : [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const searchDirs = directory ? [resolve(repoRoot, directory)] : [repoRoot];
|
|
127
|
+
const mdxFiles = [];
|
|
128
|
+
|
|
129
|
+
function walkDirectory(dir) {
|
|
130
|
+
const dirName = dir.split("/").pop();
|
|
131
|
+
if (EXCLUDED_DIRS.includes(dirName)) return;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const entries = readdirSync(dir);
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
const fullPath = join(dir, entry);
|
|
137
|
+
const stat = statSync(fullPath);
|
|
138
|
+
if (stat.isDirectory()) {
|
|
139
|
+
walkDirectory(fullPath);
|
|
140
|
+
} else if (stat.isFile() && entry.endsWith(".mdx")) {
|
|
141
|
+
mdxFiles.push(fullPath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`Error reading directory ${dir}: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const dir of searchDirs) {
|
|
150
|
+
if (existsSync(dir)) walkDirectory(dir);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return mdxFiles.sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
// Main export
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
export async function fixImages(options) {
|
|
161
|
+
const repoRoot = process.cwd();
|
|
162
|
+
|
|
163
|
+
if (!options.quiet) {
|
|
164
|
+
console.log(chalk.bold("\n🖼️ Image Frame Fixer\n"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const files = findMdxFiles(repoRoot, options.dir, options.file);
|
|
168
|
+
|
|
169
|
+
if (files.length === 0) {
|
|
170
|
+
console.error(chalk.red("✗ No MDX files found."));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!options.quiet) {
|
|
175
|
+
console.log(`Found ${files.length} MDX file(s) to process\n`);
|
|
176
|
+
if (options.dryRun) {
|
|
177
|
+
console.log(chalk.yellow("Dry run — no files will be written\n"));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const results = {};
|
|
182
|
+
let totalImages = 0;
|
|
183
|
+
|
|
184
|
+
for (const filePath of files) {
|
|
185
|
+
const content = readFileSync(filePath, "utf-8");
|
|
186
|
+
const { newContent, count } = processContent(content);
|
|
187
|
+
|
|
188
|
+
if (count > 0) {
|
|
189
|
+
const relPath = relative(repoRoot, filePath);
|
|
190
|
+
results[relPath] = count;
|
|
191
|
+
totalImages += count;
|
|
192
|
+
|
|
193
|
+
if (options.verbose) {
|
|
194
|
+
console.log(`${chalk.cyan(relPath)}: wrapped ${count} image(s)`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!options.dryRun) {
|
|
198
|
+
writeFileSync(filePath, newContent, "utf-8");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Summary
|
|
204
|
+
if (!options.quiet) {
|
|
205
|
+
const fileCount = Object.keys(results).length;
|
|
206
|
+
|
|
207
|
+
if (fileCount > 0) {
|
|
208
|
+
const verb = options.dryRun ? "Would wrap" : "Wrapped";
|
|
209
|
+
console.log(chalk.green(`\n✓ ${verb} ${totalImages} image(s) in ${fileCount} file(s)`));
|
|
210
|
+
|
|
211
|
+
if (!options.verbose) {
|
|
212
|
+
for (const [filePath, count] of Object.entries(results)) {
|
|
213
|
+
console.log(` ${chalk.cyan(filePath)}: ${count} image(s)`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
console.log(chalk.yellow("⚠️ No unwrapped images found."));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
package/src/utils/config.js
CHANGED
|
@@ -97,6 +97,69 @@ export function mergeParseConfig(options, config) {
|
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Merges config file with CLI options for the codeblocks command
|
|
102
|
+
* CLI options take precedence over config file
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} options - CLI options
|
|
105
|
+
* @param {Object|null} config - Loaded config object
|
|
106
|
+
* @returns {Object} Merged options
|
|
107
|
+
*/
|
|
108
|
+
export function mergeCodeblocksConfig(options, config) {
|
|
109
|
+
const cbConfig = config?.codeblocks || {};
|
|
110
|
+
|
|
111
|
+
// Resolve lines/wrap from config: "add" | true → add, "remove" | false → remove
|
|
112
|
+
const configLines = cbConfig.lines;
|
|
113
|
+
const configWrap = cbConfig.wrap;
|
|
114
|
+
|
|
115
|
+
const addLinesFromConfig = configLines === "add" || configLines === true;
|
|
116
|
+
const removeLinesFromConfig = configLines === "remove" || configLines === false;
|
|
117
|
+
const addWrapFromConfig = configWrap === "add" || configWrap === true;
|
|
118
|
+
const removeWrapFromConfig = configWrap === "remove" || configWrap === false;
|
|
119
|
+
|
|
120
|
+
const threshold = options.threshold != null
|
|
121
|
+
? parseInt(options.threshold, 10)
|
|
122
|
+
: (cbConfig.threshold ?? 15);
|
|
123
|
+
|
|
124
|
+
// expandable: Commander sets options.expandable=false when --no-expandable is passed
|
|
125
|
+
const expandable = options.expandable !== false
|
|
126
|
+
? (cbConfig.expandable !== false)
|
|
127
|
+
: false;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
file: options.file || cbConfig.file || null,
|
|
131
|
+
dir: options.dir || cbConfig.dir || null,
|
|
132
|
+
dryRun: options.dryRun !== undefined ? options.dryRun : (cbConfig["dry-run"] ?? false),
|
|
133
|
+
quiet: options.quiet !== undefined ? options.quiet : (cbConfig.quiet ?? false),
|
|
134
|
+
threshold,
|
|
135
|
+
expandable,
|
|
136
|
+
// CLI flags take precedence over config
|
|
137
|
+
lines: options.lines || addLinesFromConfig,
|
|
138
|
+
removeLines: options.removeLines || removeLinesFromConfig,
|
|
139
|
+
wrap: options.wrap || addWrapFromConfig,
|
|
140
|
+
removeWrap: options.removeWrap || removeWrapFromConfig,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Merges config file with CLI options for the images command
|
|
146
|
+
* CLI options take precedence over config file
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} options - CLI options
|
|
149
|
+
* @param {Object|null} config - Loaded config object
|
|
150
|
+
* @returns {Object} Merged options
|
|
151
|
+
*/
|
|
152
|
+
export function mergeImagesConfig(options, config) {
|
|
153
|
+
const imgConfig = config?.images || {};
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
file: options.file || imgConfig.file || null,
|
|
157
|
+
dir: options.dir || imgConfig.dir || null,
|
|
158
|
+
dryRun: options.dryRun !== undefined ? options.dryRun : (imgConfig["dry-run"] ?? false),
|
|
159
|
+
quiet: options.quiet !== undefined ? options.quiet : (imgConfig.quiet ?? false),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
100
163
|
/**
|
|
101
164
|
* Validates that required fields are present
|
|
102
165
|
* @param {string|undefined} baseUrl - Base URL
|