@writechoice/mint-cli 0.0.15 → 0.0.17
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 +36 -0
- package/package.json +1 -1
- package/src/commands/fix/h1.js +182 -0
- package/src/commands/fix/inlineimages.js +305 -0
- package/src/utils/config.js +38 -0
package/bin/cli.js
CHANGED
|
@@ -135,6 +135,42 @@ fix
|
|
|
135
135
|
await fixCodeblocks(mergedOptions);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
// Fix inlineimages subcommand
|
|
139
|
+
fix
|
|
140
|
+
.command("inlineimages")
|
|
141
|
+
.description("Convert inline images to <InlineImage> 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, mergeInlineImagesConfig } = await import("../src/utils/config.js");
|
|
148
|
+
const { fixInlineImages } = await import("../src/commands/fix/inlineimages.js");
|
|
149
|
+
|
|
150
|
+
const config = loadConfig();
|
|
151
|
+
const mergedOptions = mergeInlineImagesConfig(options, config);
|
|
152
|
+
mergedOptions.verbose = !mergedOptions.quiet;
|
|
153
|
+
await fixInlineImages(mergedOptions);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Fix h1 subcommand
|
|
157
|
+
fix
|
|
158
|
+
.command("h1")
|
|
159
|
+
.description("Remove duplicate H1 headings that match the frontmatter title in MDX files")
|
|
160
|
+
.option("-f, --file <path>", "Fix a single MDX file directly")
|
|
161
|
+
.option("-d, --dir <path>", "Fix MDX files in a specific directory")
|
|
162
|
+
.option("--dry-run", "Preview changes without writing files")
|
|
163
|
+
.option("--quiet", "Suppress terminal output")
|
|
164
|
+
.action(async (options) => {
|
|
165
|
+
const { loadConfig, mergeH1Config } = await import("../src/utils/config.js");
|
|
166
|
+
const { fixH1 } = await import("../src/commands/fix/h1.js");
|
|
167
|
+
|
|
168
|
+
const config = loadConfig();
|
|
169
|
+
const mergedOptions = mergeH1Config(options, config);
|
|
170
|
+
mergedOptions.verbose = !mergedOptions.quiet;
|
|
171
|
+
await fixH1(mergedOptions);
|
|
172
|
+
});
|
|
173
|
+
|
|
138
174
|
// Fix images subcommand
|
|
139
175
|
fix
|
|
140
176
|
.command("images")
|
package/package.json
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix H1 Tool
|
|
3
|
+
*
|
|
4
|
+
* Removes duplicate H1 headings that match the frontmatter title field.
|
|
5
|
+
*
|
|
6
|
+
* If the first non-empty line after frontmatter is an H1 exactly equal
|
|
7
|
+
* to the frontmatter `title`, that line (and the immediately following
|
|
8
|
+
* blank line, if any) is removed.
|
|
9
|
+
*
|
|
10
|
+
* This mirrors the behaviour of remove_double_titles.py.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
|
|
14
|
+
import { join, relative, resolve } from "path";
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
|
|
17
|
+
const EXCLUDED_DIRS = ["node_modules", ".git"];
|
|
18
|
+
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
// Frontmatter helpers
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const FRONTMATTER_RE = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
|
|
24
|
+
const TITLE_LINE_RE = /^\s*title\s*:\s*["']?(.*?)["']?\s*$/im;
|
|
25
|
+
const H1_RE = /^\s*#\s+(.*?)\s*$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the frontmatter title value, or null if not found.
|
|
29
|
+
*/
|
|
30
|
+
function extractFrontmatterTitle(content) {
|
|
31
|
+
const fmMatch = FRONTMATTER_RE.exec(content);
|
|
32
|
+
if (!fmMatch) return null;
|
|
33
|
+
const fmText = fmMatch[0];
|
|
34
|
+
const titleMatch = TITLE_LINE_RE.exec(fmText);
|
|
35
|
+
return titleMatch ? titleMatch[1].trim() : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Removes the duplicate H1 from `content` if present.
|
|
40
|
+
* Returns { newContent, changed }.
|
|
41
|
+
*/
|
|
42
|
+
function removeDuplicateH1(content, fmTitle) {
|
|
43
|
+
const fmMatch = FRONTMATTER_RE.exec(content);
|
|
44
|
+
if (!fmMatch) return { newContent: content, changed: false };
|
|
45
|
+
|
|
46
|
+
const fmEnd = fmMatch[0].length;
|
|
47
|
+
const afterFm = content.slice(fmEnd);
|
|
48
|
+
|
|
49
|
+
const lines = afterFm.split("\n");
|
|
50
|
+
|
|
51
|
+
// Find the first non-empty, non-import line after frontmatter
|
|
52
|
+
let targetIdx = null;
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
const trimmed = lines[i].trim();
|
|
55
|
+
if (trimmed === "") continue;
|
|
56
|
+
if (/^import\s/.test(trimmed)) continue; // skip import statements
|
|
57
|
+
|
|
58
|
+
const h1Match = H1_RE.exec(lines[i]);
|
|
59
|
+
if (h1Match && h1Match[1].trim() === fmTitle) {
|
|
60
|
+
targetIdx = i;
|
|
61
|
+
}
|
|
62
|
+
// Either it matched or it didn't — stop after first content line
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (targetIdx === null) return { newContent: content, changed: false };
|
|
67
|
+
|
|
68
|
+
// Remove the H1 line
|
|
69
|
+
const newLines = [...lines.slice(0, targetIdx), ...lines.slice(targetIdx + 1)];
|
|
70
|
+
|
|
71
|
+
// Remove the immediately following blank line (now at targetIdx)
|
|
72
|
+
if (newLines[targetIdx] !== undefined && newLines[targetIdx].trim() === "") {
|
|
73
|
+
newLines.splice(targetIdx, 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const newContent = content.slice(0, fmEnd) + newLines.join("\n");
|
|
77
|
+
return { newContent, changed: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// File discovery
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function findMdxFiles(repoRoot, directory = null, file = null) {
|
|
85
|
+
if (file) {
|
|
86
|
+
const fullPath = resolve(repoRoot, file);
|
|
87
|
+
return existsSync(fullPath) ? [fullPath] : [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const searchDirs = directory ? [resolve(repoRoot, directory)] : [repoRoot];
|
|
91
|
+
const mdxFiles = [];
|
|
92
|
+
|
|
93
|
+
function walkDirectory(dir) {
|
|
94
|
+
const dirName = dir.split("/").pop();
|
|
95
|
+
if (EXCLUDED_DIRS.includes(dirName)) return;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const entries = readdirSync(dir);
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
const fullPath = join(dir, entry);
|
|
101
|
+
const stat = statSync(fullPath);
|
|
102
|
+
if (stat.isDirectory()) {
|
|
103
|
+
walkDirectory(fullPath);
|
|
104
|
+
} else if (stat.isFile() && entry.endsWith(".mdx")) {
|
|
105
|
+
mdxFiles.push(fullPath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`Error reading directory ${dir}: ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const dir of searchDirs) {
|
|
114
|
+
if (existsSync(dir)) walkDirectory(dir);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return mdxFiles.sort();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
// Main export
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export async function fixH1(options) {
|
|
125
|
+
const repoRoot = process.cwd();
|
|
126
|
+
|
|
127
|
+
if (!options.quiet) {
|
|
128
|
+
console.log(chalk.bold("\n# H1 Duplicate Title Fixer\n"));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const files = findMdxFiles(repoRoot, options.dir, options.file);
|
|
132
|
+
|
|
133
|
+
if (files.length === 0) {
|
|
134
|
+
console.error(chalk.red("✗ No MDX files found."));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!options.quiet) {
|
|
139
|
+
console.log(`Found ${files.length} MDX file(s) to process\n`);
|
|
140
|
+
if (options.dryRun) {
|
|
141
|
+
console.log(chalk.yellow("Dry run — no files will be written\n"));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const changed = [];
|
|
146
|
+
|
|
147
|
+
for (const filePath of files) {
|
|
148
|
+
const content = readFileSync(filePath, "utf-8");
|
|
149
|
+
const fmTitle = extractFrontmatterTitle(content);
|
|
150
|
+
if (!fmTitle) continue;
|
|
151
|
+
|
|
152
|
+
const { newContent, changed: didChange } = removeDuplicateH1(content, fmTitle);
|
|
153
|
+
|
|
154
|
+
if (didChange) {
|
|
155
|
+
const relPath = relative(repoRoot, filePath);
|
|
156
|
+
changed.push(relPath);
|
|
157
|
+
|
|
158
|
+
if (options.verbose) {
|
|
159
|
+
console.log(`${chalk.cyan(relPath)}: removed duplicate H1`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!options.dryRun) {
|
|
163
|
+
writeFileSync(filePath, newContent, "utf-8");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!options.quiet) {
|
|
169
|
+
if (changed.length > 0) {
|
|
170
|
+
const verb = options.dryRun ? "Would remove" : "Removed";
|
|
171
|
+
console.log(chalk.green(`\n✓ ${verb} duplicate H1 in ${changed.length} file(s)`));
|
|
172
|
+
|
|
173
|
+
if (!options.verbose) {
|
|
174
|
+
for (const relPath of changed) {
|
|
175
|
+
console.log(` ${chalk.cyan(relPath)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
console.log(chalk.yellow("⚠️ No duplicate H1 headings found."));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix Inline Images Tool
|
|
3
|
+
*
|
|
4
|
+
* Converts images that appear inline within text lines to <InlineImage> components.
|
|
5
|
+
* Handles both Markdown images () and HTML <img> tags.
|
|
6
|
+
*
|
|
7
|
+
* - Inline image (has other text on the line) → <InlineImage src="url" />
|
|
8
|
+
* - Standalone image (alone on its line) → left unchanged (use fix images)
|
|
9
|
+
*
|
|
10
|
+
* Also adds the required import at the top of the file (after frontmatter):
|
|
11
|
+
* import { InlineImage } from "/snippets/InlineImage.jsx";
|
|
12
|
+
*
|
|
13
|
+
* Skips images inside:
|
|
14
|
+
* - Fenced code blocks
|
|
15
|
+
* - Inline code spans
|
|
16
|
+
* - Markdown tables
|
|
17
|
+
* - HTML tables
|
|
18
|
+
* - <Frame> blocks
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
|
|
22
|
+
import { join, relative, resolve } from "path";
|
|
23
|
+
import chalk from "chalk";
|
|
24
|
+
|
|
25
|
+
const EXCLUDED_DIRS = ["node_modules", ".git"];
|
|
26
|
+
|
|
27
|
+
const IMPORT_LINE = 'import { InlineImage } from "/snippets/InlineImage.jsx";';
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Protection patterns (multi-line regions tokenized before line processing)
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const FRAME_RE = /<Frame(?:\s[^>]*)?>[\s\S]*?<\/Frame>/gi;
|
|
34
|
+
const HTML_TABLE_RE = /<table(?:\s[^>]*)?>[\s\S]*?<\/table>/gi;
|
|
35
|
+
const MD_TABLE_RE = /^(?:\|[^\n]*\n)+(?:\|[^\n]*)?/gm;
|
|
36
|
+
const FENCE_RE = /^[ \t]*(`{3,}|~{3,})[ \t]*[^\n]*\n[\s\S]*?\n[ \t]*\1[ \t]*$/gm;
|
|
37
|
+
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
// Tokenizer helpers
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function tokenize(pattern, text, tag) {
|
|
43
|
+
const stash = [];
|
|
44
|
+
const result = text.replace(pattern, (match) => {
|
|
45
|
+
const idx = stash.length;
|
|
46
|
+
stash.push(match);
|
|
47
|
+
return `\x00${tag}${idx}\x00`;
|
|
48
|
+
});
|
|
49
|
+
return { text: result, stash };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function detokenize(text, tag, stash) {
|
|
53
|
+
const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
54
|
+
return text.replace(new RegExp(`\x00${escapedTag}(\\d+)\x00`, "g"), (_, idx) => {
|
|
55
|
+
return stash[parseInt(idx, 10)];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
// Import injection
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns the index immediately after the frontmatter closing ---, or -1 if none.
|
|
65
|
+
*/
|
|
66
|
+
function findFrontmatterEnd(content) {
|
|
67
|
+
if (!content.startsWith("---")) return -1;
|
|
68
|
+
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
69
|
+
if (!match) return -1;
|
|
70
|
+
return match[0].length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ensures the InlineImage import is present in the file.
|
|
75
|
+
* Inserts after frontmatter (if any) with an empty line below.
|
|
76
|
+
*/
|
|
77
|
+
function ensureImport(content) {
|
|
78
|
+
if (content.includes(IMPORT_LINE)) return content;
|
|
79
|
+
|
|
80
|
+
const fmEnd = findFrontmatterEnd(content);
|
|
81
|
+
|
|
82
|
+
if (fmEnd === -1) {
|
|
83
|
+
// No frontmatter — insert at top
|
|
84
|
+
return IMPORT_LINE + "\n\n" + content;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const before = content.slice(0, fmEnd);
|
|
88
|
+
const after = content.slice(fmEnd).replace(/^\n+/, ""); // normalise blank lines
|
|
89
|
+
return before + "\n" + IMPORT_LINE + "\n\n" + after;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
93
|
+
// Per-line processing
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Processes a single line: replaces inline images (those sharing the line with text).
|
|
98
|
+
* Protects inline code spans so images inside backticks are not touched.
|
|
99
|
+
* Returns { line, count }.
|
|
100
|
+
*/
|
|
101
|
+
function processLine(line) {
|
|
102
|
+
// Quick check — skip lines with no image syntax at all
|
|
103
|
+
if (!line.includes("![") && !/<img\b/i.test(line)) {
|
|
104
|
+
return { line, count: 0 };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Protect inline code spans within this line
|
|
108
|
+
const inlineCodeResult = tokenize(/`[^`\n]+`/g, line, "ICODE");
|
|
109
|
+
let text = inlineCodeResult.text;
|
|
110
|
+
|
|
111
|
+
const hasMdImage = /!\[[^\]\n]*\]\([^\)\n]+\)/.test(text);
|
|
112
|
+
const hasHtmlImg = /<img\b[^>\n]*\/?>/i.test(text);
|
|
113
|
+
|
|
114
|
+
if (!hasMdImage && !hasHtmlImg) {
|
|
115
|
+
return { line, count: 0 };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Strip all images to check whether there's other text content on the line
|
|
119
|
+
const withoutImages = text
|
|
120
|
+
.replace(/!\[[^\]\n]*\]\([^\)\n]+\)/g, "")
|
|
121
|
+
.replace(/<img\b[^>\n]*\/?>/gi, "")
|
|
122
|
+
.trim();
|
|
123
|
+
|
|
124
|
+
if (withoutImages === "") {
|
|
125
|
+
// Line contains only images (standalone) — leave for fix images command
|
|
126
|
+
return { line, count: 0 };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Replace inline markdown images ──────────────────────────────────────────
|
|
130
|
+
// Negative lookbehind (?<!\[) prevents matching linked images [](link)
|
|
131
|
+
let count = 0;
|
|
132
|
+
text = text.replace(/(?<!\[)!\[([^\]\n]*)\]\(([^\)\n]+)\)/g, (match, alt, src) => {
|
|
133
|
+
count++;
|
|
134
|
+
const altProp = alt.trim() ? ` alt="${alt.trim()}"` : "";
|
|
135
|
+
return `<InlineImage src="${src}"${altProp} />`;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── Replace inline HTML <img> tags ──────────────────────────────────────────
|
|
139
|
+
// Rename <img ...> / <img ... /> to <InlineImage ... />, preserving all attributes
|
|
140
|
+
text = text.replace(/<img\b([^>]*?)(\s*\/?)>/gi, (match, attrs) => {
|
|
141
|
+
if (!attrs.includes("src")) return match; // no src — leave as-is
|
|
142
|
+
count++;
|
|
143
|
+
const cleanAttrs = attrs.trimEnd().replace(/\/$/, "").trimEnd();
|
|
144
|
+
return `<InlineImage${cleanAttrs} />`;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Restore inline code spans
|
|
148
|
+
text = detokenize(text, "ICODE", inlineCodeResult.stash);
|
|
149
|
+
|
|
150
|
+
return { line: text, count };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
154
|
+
// Core processing
|
|
155
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Processes MDX content: replaces inline images and injects the import.
|
|
159
|
+
* Returns { newContent, count }.
|
|
160
|
+
*/
|
|
161
|
+
function processContent(content) {
|
|
162
|
+
let text = content;
|
|
163
|
+
|
|
164
|
+
// 1. Protect multi-line regions
|
|
165
|
+
const frameResult = tokenize(FRAME_RE, text, "FRAME");
|
|
166
|
+
text = frameResult.text;
|
|
167
|
+
|
|
168
|
+
const htmlTableResult = tokenize(HTML_TABLE_RE, text, "HTMLTABLE");
|
|
169
|
+
text = htmlTableResult.text;
|
|
170
|
+
|
|
171
|
+
const mdTableResult = tokenize(MD_TABLE_RE, text, "MDTABLE");
|
|
172
|
+
text = mdTableResult.text;
|
|
173
|
+
|
|
174
|
+
const fenceResult = tokenize(FENCE_RE, text, "FENCE");
|
|
175
|
+
text = fenceResult.text;
|
|
176
|
+
|
|
177
|
+
// 2. Process line by line
|
|
178
|
+
let totalCount = 0;
|
|
179
|
+
const lines = text.split("\n");
|
|
180
|
+
const processedLines = lines.map((line) => {
|
|
181
|
+
const { line: newLine, count } = processLine(line);
|
|
182
|
+
totalCount += count;
|
|
183
|
+
return newLine;
|
|
184
|
+
});
|
|
185
|
+
text = processedLines.join("\n");
|
|
186
|
+
|
|
187
|
+
// 3. Restore protected regions
|
|
188
|
+
text = detokenize(text, "FENCE", fenceResult.stash);
|
|
189
|
+
text = detokenize(text, "MDTABLE", mdTableResult.stash);
|
|
190
|
+
text = detokenize(text, "HTMLTABLE", htmlTableResult.stash);
|
|
191
|
+
text = detokenize(text, "FRAME", frameResult.stash);
|
|
192
|
+
|
|
193
|
+
// 4. Add import if any replacements were made
|
|
194
|
+
if (totalCount > 0) {
|
|
195
|
+
text = ensureImport(text);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { newContent: text, count: totalCount };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
// File discovery
|
|
203
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
function findMdxFiles(repoRoot, directory = null, file = null) {
|
|
206
|
+
if (file) {
|
|
207
|
+
const fullPath = resolve(repoRoot, file);
|
|
208
|
+
return existsSync(fullPath) ? [fullPath] : [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const searchDirs = directory ? [resolve(repoRoot, directory)] : [repoRoot];
|
|
212
|
+
const mdxFiles = [];
|
|
213
|
+
|
|
214
|
+
function walkDirectory(dir) {
|
|
215
|
+
const dirName = dir.split("/").pop();
|
|
216
|
+
if (EXCLUDED_DIRS.includes(dirName)) return;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const entries = readdirSync(dir);
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
const fullPath = join(dir, entry);
|
|
222
|
+
const stat = statSync(fullPath);
|
|
223
|
+
if (stat.isDirectory()) {
|
|
224
|
+
walkDirectory(fullPath);
|
|
225
|
+
} else if (stat.isFile() && entry.endsWith(".mdx")) {
|
|
226
|
+
mdxFiles.push(fullPath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error(`Error reading directory ${dir}: ${error.message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const dir of searchDirs) {
|
|
235
|
+
if (existsSync(dir)) walkDirectory(dir);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return mdxFiles.sort();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
242
|
+
// Main export
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
export async function fixInlineImages(options) {
|
|
246
|
+
const repoRoot = process.cwd();
|
|
247
|
+
|
|
248
|
+
if (!options.quiet) {
|
|
249
|
+
console.log(chalk.bold("\n🖼️ Inline Image Fixer\n"));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const files = findMdxFiles(repoRoot, options.dir, options.file);
|
|
253
|
+
|
|
254
|
+
if (files.length === 0) {
|
|
255
|
+
console.error(chalk.red("✗ No MDX files found."));
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!options.quiet) {
|
|
260
|
+
console.log(`Found ${files.length} MDX file(s) to process\n`);
|
|
261
|
+
if (options.dryRun) {
|
|
262
|
+
console.log(chalk.yellow("Dry run — no files will be written\n"));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const results = {};
|
|
267
|
+
let totalImages = 0;
|
|
268
|
+
|
|
269
|
+
for (const filePath of files) {
|
|
270
|
+
const content = readFileSync(filePath, "utf-8");
|
|
271
|
+
const { newContent, count } = processContent(content);
|
|
272
|
+
|
|
273
|
+
if (count > 0) {
|
|
274
|
+
const relPath = relative(repoRoot, filePath);
|
|
275
|
+
results[relPath] = count;
|
|
276
|
+
totalImages += count;
|
|
277
|
+
|
|
278
|
+
if (options.verbose) {
|
|
279
|
+
console.log(`${chalk.cyan(relPath)}: converted ${count} inline image(s)`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!options.dryRun) {
|
|
283
|
+
writeFileSync(filePath, newContent, "utf-8");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Summary
|
|
289
|
+
if (!options.quiet) {
|
|
290
|
+
const fileCount = Object.keys(results).length;
|
|
291
|
+
|
|
292
|
+
if (fileCount > 0) {
|
|
293
|
+
const verb = options.dryRun ? "Would convert" : "Converted";
|
|
294
|
+
console.log(chalk.green(`\n✓ ${verb} ${totalImages} inline image(s) in ${fileCount} file(s)`));
|
|
295
|
+
|
|
296
|
+
if (!options.verbose) {
|
|
297
|
+
for (const [filePath, count] of Object.entries(results)) {
|
|
298
|
+
console.log(` ${chalk.cyan(filePath)}: ${count} inline image(s)`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
console.log(chalk.yellow("⚠️ No inline images found."));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
package/src/utils/config.js
CHANGED
|
@@ -141,6 +141,25 @@ export function mergeCodeblocksConfig(options, config) {
|
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Merges config file with CLI options for the inlineimages 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 mergeInlineImagesConfig(options, config) {
|
|
153
|
+
const imgConfig = config?.inlineimages || {};
|
|
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
|
+
|
|
144
163
|
/**
|
|
145
164
|
* Merges config file with CLI options for the images command
|
|
146
165
|
* CLI options take precedence over config file
|
|
@@ -160,6 +179,25 @@ export function mergeImagesConfig(options, config) {
|
|
|
160
179
|
};
|
|
161
180
|
}
|
|
162
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Merges config file with CLI options for the h1 command
|
|
184
|
+
* CLI options take precedence over config file
|
|
185
|
+
*
|
|
186
|
+
* @param {Object} options - CLI options
|
|
187
|
+
* @param {Object|null} config - Loaded config object
|
|
188
|
+
* @returns {Object} Merged options
|
|
189
|
+
*/
|
|
190
|
+
export function mergeH1Config(options, config) {
|
|
191
|
+
const h1Config = config?.h1 || {};
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
file: options.file || h1Config.file || null,
|
|
195
|
+
dir: options.dir || h1Config.dir || null,
|
|
196
|
+
dryRun: options.dryRun !== undefined ? options.dryRun : (h1Config["dry-run"] ?? false),
|
|
197
|
+
quiet: options.quiet !== undefined ? options.quiet : (h1Config.quiet ?? false),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
163
201
|
/**
|
|
164
202
|
* Validates that required fields are present
|
|
165
203
|
* @param {string|undefined} baseUrl - Base URL
|