@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 +51 -253
- 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/README.md
CHANGED
|
@@ -2,307 +2,105 @@
|
|
|
2
2
|
|
|
3
3
|
CLI tool for Mintlify documentation validation and utilities.
|
|
4
4
|
|
|
5
|
-
##
|
|
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
|
-
|
|
12
|
+
## Quick Start
|
|
15
13
|
|
|
16
14
|
```bash
|
|
17
|
-
#
|
|
18
|
-
writechoice
|
|
15
|
+
# Generate a config.json with your docs URL
|
|
16
|
+
writechoice config
|
|
19
17
|
|
|
20
|
-
#
|
|
21
|
-
|
|
18
|
+
# Edit config.json and set your source URL
|
|
19
|
+
# "source": "https://docs.example.com"
|
|
22
20
|
|
|
23
|
-
#
|
|
21
|
+
# Then run any command without arguments
|
|
24
22
|
writechoice check parse
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
###
|
|
29
|
+
### Validate
|
|
45
30
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
writechoice
|
|
61
|
-
writechoice
|
|
62
|
-
writechoice
|
|
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
|
-
|
|
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 < 10`, `y > 5` → `y > 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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
57
|
+
## Configuration
|
|
143
58
|
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
277
|
-
|
|
88
|
+
**Playwright not installed:**
|
|
278
89
|
```bash
|
|
279
90
|
npx playwright install chromium
|
|
280
91
|
```
|
|
281
92
|
|
|
282
|
-
|
|
283
|
-
|
|
93
|
+
**Too many concurrent requests:**
|
|
284
94
|
```bash
|
|
285
|
-
writechoice check links
|
|
95
|
+
writechoice check links -c 10
|
|
286
96
|
```
|
|
287
97
|
|
|
288
|
-
|
|
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.
|
|
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
|
|