mirror of
https://github.com/priyanshujain/margin-mail.git
synced 2026-10-02 11:07:06 +00:00
73 lines
2.7 KiB
JavaScript
73 lines
2.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// The prose gate. Three rules, checked over every markdown file in the repository:
|
|
//
|
|
// 1. No em dash and no en dash, anywhere, including inside code fences. This is a house rule and
|
|
// it is absolute: a comma, a colon, a semicolon, brackets or a full stop, whichever fits.
|
|
// 2. No directory tree. A tree in a committed file is stale the day somebody adds a file, and
|
|
// naming the one path that matters is what a reader actually needed.
|
|
// 3. No broken relative link. A link to a file that is not there is worse than no link, because
|
|
// it reads as though the detail exists somewhere.
|
|
//
|
|
// Run by `just docs` and by CI, and worth running before any hand-off.
|
|
|
|
import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
|
|
import { dirname, join, relative, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const SKIP = new Set(["node_modules", "dist", "target", ".git", "gen", ".playwright-mcp"]);
|
|
|
|
function markdownFiles(dir) {
|
|
const found = [];
|
|
for (const entry of readdirSync(dir)) {
|
|
if (SKIP.has(entry)) continue;
|
|
const path = join(dir, entry);
|
|
if (statSync(path).isDirectory()) found.push(...markdownFiles(path));
|
|
else if (entry.endsWith(".md")) found.push(path);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// A run of box-drawing characters, or two or more lines of the ASCII form. One stray `|--` in a
|
|
// table is not a tree; four lines of them under a fenced block is.
|
|
const TREE_GLYPHS = /[├└│┬─]{2,}/;
|
|
const ASCII_TREE = /^\s*[|`+]--\s/;
|
|
|
|
const problems = [];
|
|
|
|
for (const path of markdownFiles(root)) {
|
|
const shown = relative(root, path);
|
|
const lines = readFileSync(path, "utf8").split("\n");
|
|
let asciiTreeRun = 0;
|
|
|
|
lines.forEach((line, i) => {
|
|
const at = `${shown}:${i + 1}`;
|
|
|
|
const dash = line.match(/[—–]/);
|
|
if (dash) problems.push(`${at}: ${dash[0] === "—" ? "em" : "en"} dash: ${line.trim()}`);
|
|
|
|
if (TREE_GLYPHS.test(line)) problems.push(`${at}: directory tree: ${line.trim()}`);
|
|
|
|
if (ASCII_TREE.test(line)) {
|
|
asciiTreeRun += 1;
|
|
if (asciiTreeRun === 3) problems.push(`${at}: directory tree`);
|
|
} else {
|
|
asciiTreeRun = 0;
|
|
}
|
|
|
|
for (const [, target] of line.matchAll(/\]\(([^)#\s]+)(?:#[^)\s]*)?\)/g)) {
|
|
if (/^[a-z]+:/i.test(target) || target.startsWith("/")) continue;
|
|
const resolved = resolve(dirname(path), decodeURIComponent(target));
|
|
if (!existsSync(resolved)) problems.push(`${at}: link goes nowhere: ${target}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (problems.length === 0) {
|
|
console.log("docs: clean");
|
|
process.exit(0);
|
|
}
|
|
console.error("docs: problems found");
|
|
for (const problem of problems) console.error(` ${problem}`);
|
|
process.exit(1);
|