chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* add-frontmatter.mjs — one-shot helper that ensures every documentation
|
||||
* file under docs/<sub>/*.md and docs/README.md has a YAML frontmatter
|
||||
* header with `title`, `version`, and `lastUpdated`.
|
||||
*
|
||||
* Idempotent: docs that already have a `---` block at the top are skipped
|
||||
* (the existing frontmatter is preserved as-is). Files without a leading
|
||||
* `# Title` heading fall back to the basename humanized as a title.
|
||||
*
|
||||
* Excludes: docs/i18n/, docs/screenshots/, docs/superpowers/,
|
||||
* docs/diagrams/exported/. Subfolder READMEs are included.
|
||||
*
|
||||
* Usage: node scripts/docs/add-frontmatter.mjs [--version X.Y.Z] [--date YYYY-MM-DD]
|
||||
*/
|
||||
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const DOCS_DIR = path.join(ROOT, "docs");
|
||||
|
||||
const EXCLUDE_PREFIXES = [
|
||||
"docs/i18n/",
|
||||
"docs/screenshots/",
|
||||
"docs/superpowers/",
|
||||
"docs/diagrams/exported/",
|
||||
];
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let version = "3.8.0";
|
||||
let lastUpdated = "2026-05-13";
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
if (args[i] === "--version" && args[i + 1]) {
|
||||
version = args[i + 1];
|
||||
i += 1;
|
||||
} else if (args[i] === "--date" && args[i + 1]) {
|
||||
lastUpdated = args[i + 1];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function walk(dir, files = []) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await walk(full, files);
|
||||
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function isExcluded(rel) {
|
||||
return EXCLUDE_PREFIXES.some((p) => rel === p || rel.startsWith(p));
|
||||
}
|
||||
|
||||
function hasFrontmatter(content) {
|
||||
return /^---\r?\n/.test(content);
|
||||
}
|
||||
|
||||
function extractTopHeading(content) {
|
||||
const lines = content.replace(/^/, "").split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
const m = line.match(/^#\s+(.+?)\s*$/);
|
||||
if (m) return m[1].trim();
|
||||
// Allow blank/HTML-comment lines before the first heading
|
||||
if (line.trim() === "" || /^<!--/.test(line.trim())) continue;
|
||||
// Anything else (e.g. badge image, paragraph) — no usable heading
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function humanizeBasename(filePath) {
|
||||
const base = path.basename(filePath, ".md");
|
||||
if (base.toLowerCase() === "readme") {
|
||||
const parent = path.basename(path.dirname(filePath));
|
||||
if (parent && parent !== "." && parent !== "docs") {
|
||||
const cap = parent.charAt(0).toUpperCase() + parent.slice(1);
|
||||
return `${cap} Docs`;
|
||||
}
|
||||
return "Documentation";
|
||||
}
|
||||
return base
|
||||
.replace(/[-_]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function buildFrontmatter(title) {
|
||||
// Quote title with double quotes; escape backslashes first, then double quotes.
|
||||
const safe = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
return [
|
||||
`---`,
|
||||
`title: "${safe}"`,
|
||||
`version: ${version}`,
|
||||
`lastUpdated: ${lastUpdated}`,
|
||||
`---`,
|
||||
``,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const allFiles = await walk(DOCS_DIR);
|
||||
const targets = allFiles
|
||||
.map((abs) => ({ abs, rel: path.relative(ROOT, abs).replace(/\\/g, "/") }))
|
||||
.filter(({ rel }) => !isExcluded(rel));
|
||||
|
||||
let added = 0;
|
||||
let skipped = 0;
|
||||
let noHeading = [];
|
||||
|
||||
for (const { abs, rel } of targets) {
|
||||
const content = await fs.readFile(abs, "utf8");
|
||||
if (hasFrontmatter(content)) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
let title = extractTopHeading(content);
|
||||
if (!title) {
|
||||
title = humanizeBasename(abs);
|
||||
noHeading.push(rel);
|
||||
}
|
||||
const fm = buildFrontmatter(title);
|
||||
const next = `${fm}\n${content.replace(/^/, "")}`;
|
||||
await fs.writeFile(abs, next, "utf8");
|
||||
added += 1;
|
||||
console.log(`[add-frontmatter] added → ${rel}`);
|
||||
}
|
||||
|
||||
console.log(`[add-frontmatter] done — added=${added} skipped=${skipped} total=${targets.length}`);
|
||||
if (noHeading.length > 0) {
|
||||
console.log(
|
||||
`[add-frontmatter] fallback title used (no leading H1) for: ${noHeading.join(", ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user