@writechoice/mint-cli 0.0.19 → 0.0.20
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 +1 -0
- package/package.json +2 -2
- package/src/commands/fix/codeblocks.js +2 -2
- package/src/commands/fix/h1.js +2 -2
- package/src/commands/fix/images.js +141 -4
- package/src/commands/fix/inlineimages.js +2 -2
- package/src/commands/fix/parse.js +3 -3
- package/src/commands/metadata.js +4 -4
- package/src/utils/config.js +9 -0
package/bin/cli.js
CHANGED
|
@@ -177,6 +177,7 @@ fix
|
|
|
177
177
|
.description("Wrap standalone images in <Frame> components in MDX files")
|
|
178
178
|
.option("-f, --file <path>", "Fix a single MDX file directly")
|
|
179
179
|
.option("-d, --dir <path>", "Fix MDX files in a specific directory")
|
|
180
|
+
.option("--download [url]", "Download missing local images; uses source from config or provide a URL")
|
|
180
181
|
.option("--dry-run", "Preview changes without writing files")
|
|
181
182
|
.option("--quiet", "Suppress terminal output")
|
|
182
183
|
.action(async (options) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@writechoice/mint-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "CLI tool for Mintlify documentation validation and utilities",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"wc": "bin/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "
|
|
12
|
+
"test": "node --test 'test/**/*.test.js'",
|
|
13
13
|
"dev": "node bin/cli.js",
|
|
14
14
|
"release": "bash publish.sh",
|
|
15
15
|
"postinstall": "echo \"\\n⚠️ Don't forget to install Playwright browsers:\\n npx playwright install chromium\\n\""
|
|
@@ -75,7 +75,7 @@ function splitLines(content) {
|
|
|
75
75
|
* Processes the token list for a single code block's info string.
|
|
76
76
|
* Returns { newTokens, changes }.
|
|
77
77
|
*/
|
|
78
|
-
function processInfoTokens(tokens, lineCount, lineNum, options) {
|
|
78
|
+
export function processInfoTokens(tokens, lineCount, lineNum, options) {
|
|
79
79
|
const changes = [];
|
|
80
80
|
let newTokens = [...tokens];
|
|
81
81
|
|
|
@@ -120,7 +120,7 @@ function processInfoTokens(tokens, lineCount, lineNum, options) {
|
|
|
120
120
|
* Scans MDX content for fenced code blocks and applies flag fixes.
|
|
121
121
|
* Returns { newContent, changes }.
|
|
122
122
|
*/
|
|
123
|
-
function processContent(content, options) {
|
|
123
|
+
export function processContent(content, options) {
|
|
124
124
|
const lines = splitLines(content);
|
|
125
125
|
const result = [];
|
|
126
126
|
const changes = [];
|
package/src/commands/fix/h1.js
CHANGED
|
@@ -27,7 +27,7 @@ const H1_RE = /^\s*#\s+(.*?)\s*$/;
|
|
|
27
27
|
/**
|
|
28
28
|
* Returns the frontmatter title value, or null if not found.
|
|
29
29
|
*/
|
|
30
|
-
function extractFrontmatterTitle(content) {
|
|
30
|
+
export function extractFrontmatterTitle(content) {
|
|
31
31
|
const fmMatch = FRONTMATTER_RE.exec(content);
|
|
32
32
|
if (!fmMatch) return null;
|
|
33
33
|
const fmText = fmMatch[0];
|
|
@@ -39,7 +39,7 @@ function extractFrontmatterTitle(content) {
|
|
|
39
39
|
* Removes the duplicate H1 from `content` if present.
|
|
40
40
|
* Returns { newContent, changed }.
|
|
41
41
|
*/
|
|
42
|
-
function removeDuplicateH1(content, fmTitle) {
|
|
42
|
+
export function removeDuplicateH1(content, fmTitle) {
|
|
43
43
|
const fmMatch = FRONTMATTER_RE.exec(content);
|
|
44
44
|
if (!fmMatch) return { newContent: content, changed: false };
|
|
45
45
|
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* - HTML tables
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
|
|
15
|
-
import { join, relative, resolve } from "path";
|
|
14
|
+
import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
|
|
15
|
+
import { dirname, join, relative, resolve } from "path";
|
|
16
16
|
import chalk from "chalk";
|
|
17
17
|
|
|
18
18
|
const EXCLUDED_DIRS = ["node_modules", ".git"];
|
|
@@ -72,7 +72,7 @@ function detokenize(text, tag, stash) {
|
|
|
72
72
|
* Processes MDX content and wraps standalone images in <Frame> components.
|
|
73
73
|
* Returns { newContent, count }.
|
|
74
74
|
*/
|
|
75
|
-
function processContent(content) {
|
|
75
|
+
export function processContent(content) {
|
|
76
76
|
let text = content;
|
|
77
77
|
|
|
78
78
|
// 1. Protect existing <Frame> blocks
|
|
@@ -113,6 +113,90 @@ function processContent(content) {
|
|
|
113
113
|
return { newContent: text, count };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
// Image src extraction (for --download)
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
// Extract all image src values from MDX content (markdown + HTML img)
|
|
121
|
+
export function extractImageSrcs(content) {
|
|
122
|
+
const srcs = [];
|
|
123
|
+
|
|
124
|
+
// Markdown images:  — capture the URL part
|
|
125
|
+
const mdRe = /!\[[^\]]*\]\(([^)\s"']+)/g;
|
|
126
|
+
let m;
|
|
127
|
+
while ((m = mdRe.exec(content)) !== null) {
|
|
128
|
+
srcs.push(m[1]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// HTML img tags: <img src="..." /> or <img src='...' />
|
|
132
|
+
const htmlRe = /<img\b[^>]*\bsrc=["']([^"']+)["']/gi;
|
|
133
|
+
while ((m = htmlRe.exec(content)) !== null) {
|
|
134
|
+
srcs.push(m[1]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return srcs;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Downloads missing local images from the source URL.
|
|
142
|
+
* Only attempts images with local absolute paths (starting with /).
|
|
143
|
+
* Returns { downloaded, failed } arrays and optionally writes image_download.json.
|
|
144
|
+
*/
|
|
145
|
+
async function downloadMissingImages(files, repoRoot, downloadUrl, options) {
|
|
146
|
+
const base = downloadUrl.replace(/\/$/, "");
|
|
147
|
+
|
|
148
|
+
// Collect unique local srcs across all files
|
|
149
|
+
const srcSet = new Set();
|
|
150
|
+
for (const filePath of files) {
|
|
151
|
+
const content = readFileSync(filePath, "utf-8");
|
|
152
|
+
for (const src of extractImageSrcs(content)) {
|
|
153
|
+
// Only handle root-relative paths like /images/foo.png
|
|
154
|
+
if (src.startsWith("/") && !src.startsWith("//")) {
|
|
155
|
+
srcSet.add(src);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (srcSet.size === 0) {
|
|
161
|
+
return { downloaded: [], failed: [] };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const downloaded = [];
|
|
165
|
+
const failed = [];
|
|
166
|
+
|
|
167
|
+
for (const src of srcSet) {
|
|
168
|
+
const localPath = join(repoRoot, src);
|
|
169
|
+
|
|
170
|
+
if (existsSync(localPath)) continue; // already present
|
|
171
|
+
|
|
172
|
+
const url = base + src;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const response = await fetch(url);
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
failed.push({ src, url, reason: `HTTP ${response.status}` });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!options.dryRun) {
|
|
182
|
+
mkdirSync(dirname(localPath), { recursive: true });
|
|
183
|
+
const buffer = await response.arrayBuffer();
|
|
184
|
+
writeFileSync(localPath, Buffer.from(buffer));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
downloaded.push({ src, url });
|
|
188
|
+
|
|
189
|
+
if (options.verbose) {
|
|
190
|
+
console.log(` ${chalk.green("↓")} ${src}`);
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
failed.push({ src, url, reason: err.message });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { downloaded, failed };
|
|
198
|
+
}
|
|
199
|
+
|
|
116
200
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
201
|
// File discovery
|
|
118
202
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -200,7 +284,7 @@ export async function fixImages(options) {
|
|
|
200
284
|
}
|
|
201
285
|
}
|
|
202
286
|
|
|
203
|
-
// Summary
|
|
287
|
+
// Summary — wrapping
|
|
204
288
|
if (!options.quiet) {
|
|
205
289
|
const fileCount = Object.keys(results).length;
|
|
206
290
|
|
|
@@ -217,4 +301,57 @@ export async function fixImages(options) {
|
|
|
217
301
|
console.log(chalk.yellow("⚠️ No unwrapped images found."));
|
|
218
302
|
}
|
|
219
303
|
}
|
|
304
|
+
|
|
305
|
+
// ── Download pass ──────────────────────────────────────────────────────────
|
|
306
|
+
if (!options.download) return;
|
|
307
|
+
|
|
308
|
+
if (!options.downloadUrl) {
|
|
309
|
+
console.error(chalk.red(
|
|
310
|
+
'\n✗ --download requires a source URL.\n' +
|
|
311
|
+
' Pass it after the flag: wc fix images --download https://docs.example.com\n' +
|
|
312
|
+
' Or set "source" in config.json'
|
|
313
|
+
));
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!options.quiet) {
|
|
318
|
+
console.log(chalk.bold("\n⬇️ Downloading missing images\n"));
|
|
319
|
+
if (options.dryRun) {
|
|
320
|
+
console.log(chalk.yellow("Dry run — images will not be saved\n"));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const { downloaded, failed } = await downloadMissingImages(files, repoRoot, options.downloadUrl, options);
|
|
325
|
+
|
|
326
|
+
if (!options.quiet) {
|
|
327
|
+
if (downloaded.length > 0) {
|
|
328
|
+
const verb = options.dryRun ? "Would download" : "Downloaded";
|
|
329
|
+
console.log(chalk.green(`\n✓ ${verb} ${downloaded.length} image(s)`));
|
|
330
|
+
if (!options.verbose) {
|
|
331
|
+
for (const { src } of downloaded) {
|
|
332
|
+
console.log(` ${src}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (failed.length > 0) {
|
|
338
|
+
console.log(chalk.red(`\n✗ Failed to download ${failed.length} image(s)`));
|
|
339
|
+
for (const { src, reason } of failed) {
|
|
340
|
+
console.log(` ${chalk.cyan(src)}: ${reason}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (downloaded.length === 0 && failed.length === 0) {
|
|
345
|
+
console.log(chalk.yellow("⚠️ No missing local images found."));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Write report when there are failures
|
|
350
|
+
if (failed.length > 0 && !options.dryRun) {
|
|
351
|
+
const reportPath = join(repoRoot, "image_download.json");
|
|
352
|
+
writeFileSync(reportPath, JSON.stringify({ downloaded, failed }, null, 2), "utf-8");
|
|
353
|
+
if (!options.quiet) {
|
|
354
|
+
console.log(`\nReport written to ${chalk.cyan("image_download.json")}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
220
357
|
}
|
|
@@ -74,7 +74,7 @@ function findFrontmatterEnd(content) {
|
|
|
74
74
|
* Ensures the InlineImage import is present in the file.
|
|
75
75
|
* Inserts after frontmatter (if any) with an empty line below.
|
|
76
76
|
*/
|
|
77
|
-
function ensureImport(content) {
|
|
77
|
+
export function ensureImport(content) {
|
|
78
78
|
if (content.includes(IMPORT_LINE)) return content;
|
|
79
79
|
|
|
80
80
|
const fmEnd = findFrontmatterEnd(content);
|
|
@@ -158,7 +158,7 @@ function processLine(line) {
|
|
|
158
158
|
* Processes MDX content: replaces inline images and injects the import.
|
|
159
159
|
* Returns { newContent, count }.
|
|
160
160
|
*/
|
|
161
|
-
function processContent(content) {
|
|
161
|
+
export function processContent(content) {
|
|
162
162
|
let text = content;
|
|
163
163
|
|
|
164
164
|
// 1. Protect multi-line regions
|
|
@@ -89,7 +89,7 @@ function getFilesFromReport(reportPath, repoRoot) {
|
|
|
89
89
|
* Splits file content into protected (code) and unprotected (text) segments.
|
|
90
90
|
* Returns an array of { text, protected } objects.
|
|
91
91
|
*/
|
|
92
|
-
function segmentContent(content) {
|
|
92
|
+
export function segmentContent(content) {
|
|
93
93
|
const segments = [];
|
|
94
94
|
let pos = 0;
|
|
95
95
|
const len = content.length;
|
|
@@ -154,7 +154,7 @@ function segmentContent(content) {
|
|
|
154
154
|
* Fixes void HTML tags in a text segment (not inside inline code).
|
|
155
155
|
* Returns { text, count }.
|
|
156
156
|
*/
|
|
157
|
-
function fixVoidTags(text) {
|
|
157
|
+
export function fixVoidTags(text) {
|
|
158
158
|
let count = 0;
|
|
159
159
|
|
|
160
160
|
// Process the text but protect inline code spans
|
|
@@ -205,7 +205,7 @@ function replaceVoidTags(text) {
|
|
|
205
205
|
* Fixes stray < and > in a text segment (not inside inline code or tags).
|
|
206
206
|
* Returns { text, count }.
|
|
207
207
|
*/
|
|
208
|
-
function fixStrayAngleBrackets(text) {
|
|
208
|
+
export function fixStrayAngleBrackets(text) {
|
|
209
209
|
let count = 0;
|
|
210
210
|
|
|
211
211
|
// Process the text but protect inline code spans and valid tags
|
package/src/commands/metadata.js
CHANGED
|
@@ -56,7 +56,7 @@ function parseHtmlAttributes(attrStr) {
|
|
|
56
56
|
* Looks at property, name, and itemprop attributes.
|
|
57
57
|
* Returns { "og:title": "...", ... }
|
|
58
58
|
*/
|
|
59
|
-
function extractMetaTags(html, tags) {
|
|
59
|
+
export function extractMetaTags(html, tags) {
|
|
60
60
|
const results = {};
|
|
61
61
|
const metaRe = /<meta\s+([^>]+?)(?:\s*\/?>)/gi;
|
|
62
62
|
let m;
|
|
@@ -120,7 +120,7 @@ async function runConcurrent(tasks, concurrency) {
|
|
|
120
120
|
// URL construction
|
|
121
121
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
122
|
|
|
123
|
-
function fileToUrl(filePath, scanDir, baseUrl) {
|
|
123
|
+
export function fileToUrl(filePath, scanDir, baseUrl) {
|
|
124
124
|
const rel = relative(scanDir, filePath)
|
|
125
125
|
.replace(/\.mdx$/, "")
|
|
126
126
|
.replace(/\\/g, "/");
|
|
@@ -141,7 +141,7 @@ function escapeRe(str) {
|
|
|
141
141
|
* Formats a string value for YAML output.
|
|
142
142
|
* Always produces a quoted scalar to avoid YAML interpretation issues.
|
|
143
143
|
*/
|
|
144
|
-
function yamlValue(str) {
|
|
144
|
+
export function yamlValue(str) {
|
|
145
145
|
if (!str.includes('"')) return `"${str}"`;
|
|
146
146
|
if (!str.includes("'")) return `'${str}'`;
|
|
147
147
|
// Both quotes present — escape double quotes
|
|
@@ -153,7 +153,7 @@ function yamlValue(str) {
|
|
|
153
153
|
* Updates existing frontmatter keys, appends missing ones.
|
|
154
154
|
* Returns { newContent, updated: string[], added: string[], skipped: boolean }
|
|
155
155
|
*/
|
|
156
|
-
function applyMetaToContent(content, metaData) {
|
|
156
|
+
export function applyMetaToContent(content, metaData) {
|
|
157
157
|
const fmMatch = FRONTMATTER_RE.exec(content);
|
|
158
158
|
if (!fmMatch) {
|
|
159
159
|
return { newContent: content, updated: [], added: [], skipped: true };
|
package/src/utils/config.js
CHANGED
|
@@ -171,11 +171,20 @@ export function mergeInlineImagesConfig(options, config) {
|
|
|
171
171
|
export function mergeImagesConfig(options, config) {
|
|
172
172
|
const imgConfig = config?.images || {};
|
|
173
173
|
|
|
174
|
+
// download: true if --download was passed without a URL, string if URL provided
|
|
175
|
+
const downloadFlag = options.download !== undefined ? options.download : (imgConfig.download ?? false);
|
|
176
|
+
let downloadUrl = null;
|
|
177
|
+
if (downloadFlag) {
|
|
178
|
+
downloadUrl = typeof downloadFlag === "string" ? downloadFlag : (config?.source || null);
|
|
179
|
+
}
|
|
180
|
+
|
|
174
181
|
return {
|
|
175
182
|
file: options.file || imgConfig.file || null,
|
|
176
183
|
dir: options.dir || imgConfig.dir || null,
|
|
177
184
|
dryRun: options.dryRun !== undefined ? options.dryRun : (imgConfig["dry-run"] ?? false),
|
|
178
185
|
quiet: options.quiet !== undefined ? options.quiet : (imgConfig.quiet ?? false),
|
|
186
|
+
download: !!downloadFlag,
|
|
187
|
+
downloadUrl,
|
|
179
188
|
};
|
|
180
189
|
}
|
|
181
190
|
|