|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +/* eslint-disable no-console */ |
| 4 | + |
| 5 | +/** |
| 6 | + * A simple Markdown linter CLI using markdownlint with VS Code-like defaults. |
| 7 | + * |
| 8 | + * Usage: |
| 9 | + * node mdlint.js --only MD052 content |
| 10 | + */ |
| 11 | + |
| 12 | +const fs = require("fs"); |
| 13 | +const path = require("path"); |
| 14 | +let markdownlint; |
| 15 | +try { |
| 16 | + markdownlint = require("markdownlint"); // ensure 0.26.1 installed |
| 17 | +} catch { |
| 18 | + console.error("Please run: npm i -D markdownlint@0.26.1"); |
| 19 | + process.exit(2); |
| 20 | +} |
| 21 | + |
| 22 | +// Parse simple flags: --only MD052,MD013 ... |
| 23 | +function parseArgs(argv) { |
| 24 | + const args = []; |
| 25 | + const flags = {}; |
| 26 | + for (let i = 0; i < argv.length; i++) { |
| 27 | + const a = argv[i]; |
| 28 | + if (a === "--only") { |
| 29 | + flags.only = (argv[++i] || "").split(",").map(s => s.trim()).filter(Boolean); |
| 30 | + } else if (a.startsWith("--only=")) { |
| 31 | + flags.only = a.slice("--only=".length).split(",").map(s => s.trim()).filter(Boolean); |
| 32 | + } else { |
| 33 | + args.push(a); |
| 34 | + } |
| 35 | + } |
| 36 | + return { paths: args, flags }; |
| 37 | +} |
| 38 | + |
| 39 | +// Recursively collect .md files |
| 40 | +function collect(target) { |
| 41 | + const abs = path.resolve(target); |
| 42 | + if (!fs.existsSync(abs)) throw new Error(`Path does not exist: ${target}`); |
| 43 | + const st = fs.statSync(abs); |
| 44 | + if (st.isDirectory()) { |
| 45 | + const out = []; |
| 46 | + for (const de of fs.readdirSync(abs, { withFileTypes: true })) { |
| 47 | + const p = path.join(abs, de.name); |
| 48 | + if (de.isDirectory()) out.push(...collect(p)); |
| 49 | + else if (de.isFile() && p.toLowerCase().endsWith(".md")) out.push(p); |
| 50 | + } |
| 51 | + return out; |
| 52 | + } |
| 53 | + return st.isFile() && abs.toLowerCase().endsWith(".md") ? [abs] : []; |
| 54 | +} |
| 55 | + |
| 56 | +function buildConfig(only) { |
| 57 | + if (!only || only.length === 0) { |
| 58 | + return { default: true }; // VS Code-like defaults |
| 59 | + } |
| 60 | + // Normalize to rule code form (e.g., md052 -> MD052) |
| 61 | + const enabled = new Set(only.map(s => s.toUpperCase())); |
| 62 | + const cfg = { default: false }; |
| 63 | + for (const code of enabled) cfg[code] = true; |
| 64 | + |
| 65 | + // Optional: also turn on known aliases for MD052 to be extra robust |
| 66 | + if (enabled.has("MD052")) cfg["reference-links-images"] = true; |
| 67 | + |
| 68 | + return cfg; |
| 69 | +} |
| 70 | + |
| 71 | +function lint(files, config) { |
| 72 | + if (files.length === 0) { |
| 73 | + console.log("No Markdown files to lint."); |
| 74 | + return 0; |
| 75 | + } |
| 76 | + const result = markdownlint.sync({ |
| 77 | + files, |
| 78 | + config, |
| 79 | + resultVersion: 3 |
| 80 | + }); |
| 81 | + |
| 82 | + let issues = 0; |
| 83 | + for (const file of Object.keys(result)) { |
| 84 | + const problems = result[file]; |
| 85 | + if (!problems.length) continue; |
| 86 | + console.log(`\n${file}`); |
| 87 | + for (const p of problems) { |
| 88 | + issues++; |
| 89 | + const loc = `${p.lineNumber}${p.errorRange ? ":" + p.errorRange[0] : ""}`; |
| 90 | + const msg = |
| 91 | + `${p.ruleNames.join("/")}: ${p.ruleDescription}` + |
| 92 | + (p.errorDetail ? ` [${p.errorDetail}]` : "") + |
| 93 | + (p.errorContext ? ` [Context: ${p.errorContext}]` : ""); |
| 94 | + console.log(` ${loc} ${msg}`); |
| 95 | + } |
| 96 | + } |
| 97 | + if (issues === 0) console.log(`✔ No issues found in ${files.length} file(s).`); |
| 98 | + else console.log(`\n✖ ${issues} issue(s) total in ${files.length} file(s).`); |
| 99 | + return issues === 0 ? 0 : 1; |
| 100 | +} |
| 101 | + |
| 102 | +(function main() { |
| 103 | + const { paths, flags } = parseArgs(process.argv.slice(2)); |
| 104 | + if (paths.length === 0) { |
| 105 | + console.error("Usage: mdlint [--only MD052[,MD013...]] <file-or-folder> [...]"); |
| 106 | + process.exit(1); |
| 107 | + } |
| 108 | + let files = []; |
| 109 | + for (const p of paths) { |
| 110 | + try { files = files.concat(collect(p)); } |
| 111 | + catch (e) { console.error(e.message); process.exit(1); } |
| 112 | + } |
| 113 | + const config = buildConfig(flags.only); |
| 114 | + const code = lint(files, config); |
| 115 | + process.exit(code); |
| 116 | +})(); |
0 commit comments