website/scripts/new-content.js
2026-04-14 17:07:49 +02:00

185 lines
4.7 KiB
JavaScript

import fs from "node:fs/promises";
import path from "node:path";
function printUsage() {
console.log(
"Usage: pnpm new-content <target> [--title <title>] [--date <date>] [--author <author>] [--force]",
);
console.log("Examples:");
console.log(" pnpm new-content events/new-event");
console.log(
' pnpm new-content content/projects/new-tool.md --title "New Tool"',
);
}
function parseArgs(argv) {
const args = argv.slice(2);
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
printUsage();
process.exit(0);
}
const options = {
target: null,
title: null,
date: null,
author: null,
force: false,
};
let i = 0;
while (i < args.length) {
const current = args[i];
if (!current.startsWith("--") && options.target === null) {
options.target = current;
i += 1;
continue;
}
if (current === "--force") {
options.force = true;
i += 1;
continue;
}
if (
current === "--title" ||
current === "--date" ||
current === "--author"
) {
const value = args[i + 1];
if (!value || value.startsWith("--")) {
throw new Error(`Missing value for ${current}`);
}
if (current === "--title") options.title = value;
if (current === "--date") options.date = value;
if (current === "--author") options.author = value;
i += 2;
continue;
}
throw new Error(`Unknown argument: ${current}`);
}
if (!options.target) {
throw new Error("Missing target path");
}
return options;
}
function toLocalIsoWithTimezone(date = new Date()) {
const pad = (n) => String(n).padStart(2, "0");
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds());
const tzMinutes = -date.getTimezoneOffset();
const sign = tzMinutes >= 0 ? "+" : "-";
const tzAbs = Math.abs(tzMinutes);
const tzHours = pad(Math.floor(tzAbs / 60));
const tzMins = pad(tzAbs % 60);
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${sign}${tzHours}:${tzMins}`;
}
function slugToTitle(filePath) {
const slug = path.basename(filePath, ".md");
return slug
.replace(/[-_]+/g, " ")
.replace(/\s+/g, " ")
.trim()
.replace(/\b\w/g, (ch) => ch.toUpperCase());
}
function escapeYamlString(value) {
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
function resolveTargetPath(targetArg, contentRoot) {
let normalized = targetArg.replace(/\\/g, "/").replace(/^\.\//, "");
if (normalized.startsWith("content/")) {
normalized = normalized.slice("content/".length);
}
if (!normalized || normalized.endsWith("/")) {
throw new Error(
"Target must be a markdown file path, for example: events/my-event.md",
);
}
if (!normalized.toLowerCase().endsWith(".md")) {
normalized = `${normalized}.md`;
}
const absolute = path.resolve(contentRoot, normalized);
const contentPrefix = contentRoot.endsWith(path.sep)
? contentRoot
: `${contentRoot}${path.sep}`;
if (absolute !== contentRoot && !absolute.startsWith(contentPrefix)) {
throw new Error("Target path must stay inside content/");
}
return absolute;
}
async function main() {
const { target, title, date, author, force } = parseArgs(process.argv);
const repoRoot = process.cwd();
const contentRoot = path.resolve(repoRoot, "content");
const archetypePath = path.resolve(repoRoot, "archetypes", "default.md");
const targetPath = resolveTargetPath(target, contentRoot);
const resolvedTitle = title ?? slugToTitle(targetPath);
const resolvedDate = date ?? toLocalIsoWithTimezone(new Date());
const resolvedAuthor = author ?? "ADMStaff";
const [template, existing] = await Promise.all([
fs.readFile(archetypePath, "utf8"),
fs
.access(targetPath)
.then(() => true)
.catch(() => false),
]);
if (existing && !force) {
throw new Error(
`File already exists: ${path.relative(repoRoot, targetPath)} (use --force to overwrite)`,
);
}
let output = template;
output = output.replace(
/^title:\s*.*$/m,
`title: "${escapeYamlString(resolvedTitle)}"`,
);
output = output.replace(/^date:\s*.*$/m, `date: ${resolvedDate}`);
output = output.replace(
/^author:\s*.*$/m,
`author: "${escapeYamlString(resolvedAuthor)}"`,
);
if (!output.endsWith("\n")) {
output += "\n";
}
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, output, "utf8");
console.log(`Created: ${path.relative(repoRoot, targetPath)}`);
}
main().catch((error) => {
console.error(`Error: ${error.message}`);
process.exit(1);
});