Files

144 lines
4.2 KiB
JavaScript

/* 创建一篇新文章
*
* 用法:
* pnpm new-post "文章标题"
* pnpm new-post "树莓派装 Docker" --category 教程 --tags Docker,树莓派
* pnpm new-post "读《XX》有感" --category 思考 --description "摘要" --pinned
* pnpm new-post "标题" --file 文件名 --slug my-custom-slug --category 建站
*
* 不加 --category 时文件放在 src/content/posts/ 根目录;
* 加了则同时写进 frontmatter 的 category 并放进同名子目录,和现有文章结构一致。
*/
import fs from "node:fs"
import path from "node:path"
const POSTS_DIR = "./src/content/posts"
function today() {
const d = new Date()
const p = (n) => String(n).padStart(2, "0")
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
}
const args = process.argv.slice(2)
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
// 无参数进来时先说清楚原因:下面 pnpm 还会补一句
// "[ELIFECYCLE] Command failed with exit code 1",不点明的话很像脚本崩了
if (args.length === 0) {
console.log('缺少文章标题,没有创建任何文件。\n正确写法:pnpm new-post "树莓派装 Docker 全程记录" --category 教程\n')
}
console.log(`创建一篇新文章
用法:
pnpm new-post "文章标题" [选项]
选项:
--category <分类> 分类名,同时决定存放的子目录
--file <文件名> 自定义文件名(默认用标题)
--tags <标签,标签> 标签,逗号分隔
--slug <英文主题> 通常使用文章标题的英文缩写,Astro博客自带的属性
--description <摘要> 摘要,用于 SEO 和列表卡片
--image <路径> 封面图,public 下的绝对路径
--pinned 置顶
--draft 存为草稿(构建时不会输出)
示例:
pnpm new-post "树莓派装 Docker" --category 教程 --tags Docker,树莓派`)
process.exit(args.length === 0 ? 1 : 0)
}
const title = args[0]
const opts = {
category: "",
file: "",
tags: "",
description: "",
image: "",
pinned: false,
draft: false,
}
for (let i = 1; i < args.length; i++) {
const arg = args[i]
if (arg === "--pinned") {
opts.pinned = true
continue
}
if (arg === "--draft") {
opts.draft = true
continue
}
if (!arg.startsWith("--")) {
console.error(`错误:无法识别的参数 "${arg}"`)
process.exit(1)
}
const key = arg.slice(2)
if (!(key in opts)) {
console.error(`错误:未知选项 --${key}`)
process.exit(1)
}
const value = args[i + 1]
if (value === undefined || value.startsWith("--")) {
console.error(`错误:--${key} 缺少值`)
process.exit(1)
}
opts[key] = value
i++
}
/** 文件名里不能出现 / : 等字符,统一替换掉 */
const safeName = (s) =>
s
.replace(/[/\\:*?"<>|]/g, "")
.replace(/\s+/g, "-")
.trim()
const fileName = `${safeName(opts.file || title)}.md`
const dir = opts.category ? path.join(POSTS_DIR, safeName(opts.category)) : POSTS_DIR
const fullPath = path.join(dir, fileName)
if (fs.existsSync(fullPath)) {
console.error(`错误:${fullPath} 已存在`)
process.exit(1)
}
fs.mkdirSync(dir, { recursive: true })
const list = (v) =>
v
? v
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: []
const tags = list(opts.tags)
/** 双引号包裹并转义,避免标题里的冒号、引号破坏 YAML */
const q = (v) => `"${String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
const frontmatter = [
`title: ${q(title)}`,
`published: ${today()}`, // 只写日期,YAML 按 UTC 零点解析,不会有时区偏移
opts.description ? `description: ${q(opts.description)}` : "",
opts.image ? `image: ${q(opts.image)}` : "",
tags.length > 0 ? `tags:\n${tags.map((t) => ` - ${q(t)}`).join("\n")}` : "tags: []",
`category: ${q(opts.category)}`,
opts.pinned ? "pinned: true" : "",
opts.draft ? "draft: true" : "",
]
.filter(Boolean)
.join("\n")
fs.writeFileSync(fullPath, `---\n${frontmatter}\n---\n\n在这里写正文。\n`, "utf8")
console.log(`已创建文章: ${fullPath}`)
if (opts.draft) console.log("状态:草稿(draft: true),构建时不会输出。写完记得删掉这一行。")
console.log("运行 pnpm dev 可以在浏览器里实时预览。")