@writechoice/mint-cli 0.0.18 → 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/README.md CHANGED
@@ -2,307 +2,105 @@
2
2
 
3
3
  CLI tool for Mintlify documentation validation and utilities.
4
4
 
5
- ## Quick Start
6
-
7
- ### Installation
5
+ ## Installation
8
6
 
9
7
  ```bash
10
8
  npm install -g @writechoice/mint-cli
11
9
  npx playwright install chromium
12
10
  ```
13
11
 
14
- ### Usage
12
+ ## Quick Start
15
13
 
16
14
  ```bash
17
- # Check version
18
- writechoice --version
15
+ # Generate a config.json with your docs URL
16
+ writechoice config
19
17
 
20
- # Update to latest
21
- writechoice update
18
+ # Edit config.json and set your source URL
19
+ # "source": "https://docs.example.com"
22
20
 
23
- # Validate MDX parsing
21
+ # Then run any command without arguments
24
22
  writechoice check parse
25
-
26
- # Validate links
27
- writechoice check links https://docs.example.com
28
-
29
- # Validate with local development server
30
- writechoice check links docs.example.com http://localhost:3000
31
-
32
- # Fix broken anchor links
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
23
+ writechoice check links
24
+ writechoice metadata
40
25
  ```
41
26
 
42
27
  ## Commands
43
28
 
44
- ### `config`
29
+ ### Validate
45
30
 
46
- Generates a config.json template file with all available options.
31
+ | Command | Description |
32
+ |---|---|
33
+ | `writechoice check parse` | Validate MDX files for parsing errors |
34
+ | `writechoice check links [baseUrl]` | Validate internal links and anchors using Playwright |
47
35
 
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
-
55
- ### `check parse`
36
+ ### Fix
56
37
 
57
- Validates MDX files for parsing errors.
58
-
59
- ```bash
60
- writechoice check parse # All files
61
- writechoice check parse -f file.mdx # Single file
62
- writechoice check parse -d docs # Specific directory
63
- ```
38
+ | Command | Description |
39
+ |---|---|
40
+ | `writechoice fix parse` | Fix void tags and stray angle brackets |
41
+ | `writechoice fix links` | Fix broken anchor links from a validation report |
42
+ | `writechoice fix codeblocks` | Add/remove `expandable`, `lines`, `wrap` on code blocks |
43
+ | `writechoice fix images` | Wrap standalone images in `<Frame>` |
44
+ | `writechoice fix inlineimages` | Convert inline images to `<InlineImage>` |
45
+ | `writechoice fix h1` | Remove H1 headings that duplicate the frontmatter title |
64
46
 
65
- **Output:** Both `mdx_errors_report.json` and `mdx_errors_report.md`
66
-
67
- ### `check links`
68
-
69
- Validates internal links and anchors using browser automation.
70
-
71
- ```bash
72
- writechoice check links <baseUrl> [validationBaseUrl]
73
- ```
74
-
75
- **Common options:**
76
- - `-f, --file <path>` - Validate single file
77
- - `-d, --dir <path>` - Validate specific directory
78
- - `-o, --output <path>` - Base name for reports (default: `links_report`)
79
- - `-c, --concurrency <number>` - Concurrent browser tabs (default: 25)
80
- - `--quiet` - Suppress output
81
- - `--dry-run` - Extract links without validating
82
-
83
- **Output:** Both `links_report.json` and `links_report.md`
84
-
85
- ### `fix links`
86
-
87
- Automatically fixes broken anchor links based on validation reports.
88
-
89
- ```bash
90
- writechoice fix links # Use default report
91
- writechoice fix links -r custom_report.json # Use custom report
92
- ```
93
-
94
- **Common options:**
95
- - `-r, --report <path>` - Path to JSON report (default: `links_report.json`)
96
- - `--quiet` - Suppress output
97
-
98
- **Note:** Requires JSON report from `check links` command.
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 &lt; 10`, `y > 5` → `y &gt; 5`
120
-
121
- Content inside code blocks and inline code is never modified.
122
-
123
- ### `update`
124
-
125
- Update CLI to latest version.
126
-
127
- ```bash
128
- writechoice update
129
- ```
47
+ ### Other
130
48
 
131
- ## Features
49
+ | Command | Description |
50
+ |---|---|
51
+ | `writechoice metadata` | Fetch meta tags from live pages and write to frontmatter |
52
+ | `writechoice config` | Generate a `config.json` template |
53
+ | `writechoice update` | Update to the latest version |
132
54
 
133
- - **MDX Parsing Validation** - Catch syntax errors before deployment
134
- - **Link Validation** - Test links against live websites with Playwright
135
- - **Two-Step Anchor Validation** - Compare production vs development anchors
136
- - **Auto-Fix Links** - Automatically correct broken anchor links
137
- - **Auto-Fix Parsing** - Automatically fix void tags and stray angle brackets
138
- - **Dual Report Formats** - Generates both JSON (for automation) and Markdown (for humans)
139
- - **Configuration File** - Optional config.json for default settings
140
- - **CI/CD Ready** - Exit codes for pipeline integration
55
+ All commands support `--file <path>`, `--dir <path>`, `--dry-run`, and `--quiet` where applicable.
141
56
 
142
- ## How Two-Step Validation Works
57
+ ## Configuration
143
58
 
144
- For anchor links, the tool:
145
-
146
- 1. **Finds the target** (Production): Navigates to production docs to identify which heading the anchor points to
147
- 2. **Gets the anchor** (Validation): Navigates to your dev server (localhost:3000), clicks the heading, and extracts the generated anchor
148
- 3. **Compares**: Checks if anchors match
149
-
150
- This ensures your local development environment matches production behavior.
151
-
152
- ## Configuration File
153
-
154
- Create an optional `config.json` in your project root to set default values:
59
+ Create a `config.json` in your project root to set defaults and avoid repeating arguments:
155
60
 
156
61
  ```json
157
62
  {
158
63
  "source": "https://docs.example.com",
159
- "target": "http://localhost:3000",
160
- "links": {
161
- "concurrency": 25,
162
- "quiet": false
163
- },
164
- "parse": {
165
- "quiet": false
166
- }
64
+ "target": "http://localhost:3000"
167
65
  }
168
66
  ```
169
67
 
170
- With config.json, you can run commands without arguments:
171
-
172
- ```bash
173
- # Uses source and target from config.json
174
- writechoice check links
175
-
176
- # CLI args override config.json values
177
- writechoice check links https://staging.example.com
178
- ```
179
-
180
- See [config.example.json](config.example.json) for all available options.
181
-
182
- ## Examples
183
-
184
- ```bash
185
- # Validate all MDX files for parsing errors
186
- writechoice check parse
187
-
188
- # Validate all links (uses localhost:3000 by default)
189
- writechoice check links https://docs.example.com
190
-
191
- # Validate with staging environment
192
- writechoice check links docs.example.com https://staging.example.com
193
-
194
- # Validate specific directory only
195
- writechoice check links docs.example.com -d api
196
-
197
- # Run quietly (only generate reports)
198
- writechoice check links docs.example.com --quiet
199
-
200
- # Fix broken anchor links
201
- writechoice fix links
202
-
203
- # Fix from custom report
204
- writechoice fix links -r custom_report.json
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
68
+ Run `writechoice config` to generate a full template with all options.
211
69
 
212
- # Full workflow: validate -> fix -> re-validate
213
- writechoice check links docs.example.com
214
- writechoice fix links
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
221
- ```
70
+ See [docs/config-file.md](docs/config-file.md) for the complete reference.
222
71
 
223
72
  ## Documentation
224
73
 
225
- Detailed documentation is available in the [docs/](docs/) folder:
226
-
227
- - **Commands**
228
- - [config](docs/commands/config.md) - Generate config.json template
229
- - [check links](docs/commands/check-links.md) - Link validation
230
- - [check parse](docs/commands/check-parse.md) - MDX parsing validation
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
233
- - [update](docs/commands/update.md) - Update command
234
- - **Guides**
235
- - [Configuration File](docs/config-file.md) - Using config.json
236
- - [Configuration](docs/configuration.md) - Advanced configuration
237
- - [Publishing](docs/publishing.md) - How to publish new versions
238
-
239
- ## Development
240
-
241
- ### Local Setup
242
-
243
- ```bash
244
- git clone <repository-url>
245
- cd writechoice-mint-cli
246
- npm install
247
- npx playwright install chromium
248
- chmod +x bin/cli.js
249
- npm link
250
- ```
251
-
252
- ### Project Structure
74
+ Full command documentation is in [docs/commands/](docs/commands/):
253
75
 
254
- ```
255
- writechoice-mint-cli/
256
- ├── bin/
257
- │ └── cli.js # CLI entry point
258
- ├── src/
259
- │ ├── commands/
260
- │ │ ├── validate/
261
- │ │ │ ├── links.js # Link validation
262
- │ │ │ └── mdx.js # MDX parsing validation
263
- │ │ └── fix/
264
- │ │ ├── links.js # Link fixing
265
- │ │ └── parse.js # Parse error fixing
266
- │ └── utils/
267
- │ ├── helpers.js # Utility functions
268
- │ └── reports.js # Report generation
269
- ├── docs/ # Detailed documentation
270
- ├── package.json
271
- └── README.md
272
- ```
76
+ - [check-links](docs/commands/check-links.md)
77
+ - [check-parse](docs/commands/check-parse.md)
78
+ - [fix-links](docs/commands/fix-links.md)
79
+ - [fix-parse](docs/commands/fix-parse.md)
80
+ - [fix-codeblocks](docs/commands/fix-codeblocks.md)
81
+ - [fix-images](docs/commands/fix-images.md)
82
+ - [fix-inlineimages](docs/commands/fix-inlineimages.md)
83
+ - [fix-h1](docs/commands/fix-h1.md)
84
+ - [metadata](docs/commands/metadata.md)
273
85
 
274
86
  ## Troubleshooting
275
87
 
276
- ### Playwright Not Installed
277
-
88
+ **Playwright not installed:**
278
89
  ```bash
279
90
  npx playwright install chromium
280
91
  ```
281
92
 
282
- ### Memory Issues
283
-
93
+ **Too many concurrent requests:**
284
94
  ```bash
285
- writechoice check links docs.example.com -c 10
95
+ writechoice check links -c 10
286
96
  ```
287
97
 
288
- ### Permission Errors
289
-
98
+ **Permission error on install:**
290
99
  ```bash
291
100
  sudo npm install -g @writechoice/mint-cli
101
+ # or use nvm: https://github.com/nvm-sh/nvm
292
102
  ```
293
103
 
294
- Or use a node version manager like [nvm](https://github.com/nvm-sh/nvm).
295
-
296
104
  ## License
297
105
 
298
- MIT
299
-
300
- ## Contributing
301
-
302
- Contributions are welcome! Please open an issue or submit a pull request.
303
-
304
- ## Links
305
-
306
- - [npm package](https://www.npmjs.com/package/@writechoice/mint-cli)
307
- - [GitHub repository](https://github.com/writechoice/mint-cli)
308
- - [Issue tracker](https://github.com/writechoice/mint-cli/issues)
106
+ MIT — [GitHub](https://github.com/writechoice/mint-cli) · [npm](https://www.npmjs.com/package/@writechoice/mint-cli) · [Issues](https://github.com/writechoice/mint-cli/issues)
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.18",
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": "echo \"Error: no test specified\" && exit 1",
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 = [];
@@ -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: ![alt](src) — 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
@@ -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 };
@@ -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