diff --git a/package.json b/package.json index a87befe..f17b33f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "preview": "astro preview", "astro": "astro", "lint": "eslint ./src --fix && stylelint ./src/**/*.{scss,css,astro} --fix && astro check", - "format": "prettier --write ./src" + "format": "prettier --write ./src", + "new": "node scripts/new.mjs", + "pub": "node scripts/pub.mjs" }, "dependencies": { "@astrojs/rss": "^4.0.11", diff --git a/scaffolds/draft.md b/scaffolds/draft.md new file mode 100644 index 0000000..08a8f78 --- /dev/null +++ b/scaffolds/draft.md @@ -0,0 +1,8 @@ +--- +title: {{ title }} +slug: +category: +tags: +cover: +description: +--- diff --git a/scaffolds/post.md b/scaffolds/post.md new file mode 100644 index 0000000..eaea4d8 --- /dev/null +++ b/scaffolds/post.md @@ -0,0 +1,9 @@ +--- +title: {{ title }} +slug: +category: +tags: +cover: +description: +published: {{ date }} +--- diff --git a/scripts/new.mjs b/scripts/new.mjs new file mode 100644 index 0000000..119e53d --- /dev/null +++ b/scripts/new.mjs @@ -0,0 +1,144 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises'; +import path from 'node:path'; +import readline from 'node:readline'; + +const args = process.argv.slice(2); +let type, + name, + isDir = false; + +for (let i = 0; i < args.length; i++) { + if (args[i] === '--dir') { + isDir = true; + continue; + } + if (!type) { + type = args[i]; + } else if (!name) { + name = args[i]; + } +} + +if (!type || !name) { + console.error('Usage: pnpm new [post|draft] [name] [--dir]'); + process.exit(1); +} + +if (type !== 'post' && type !== 'draft') { + console.error('Type must be either "post" or "draft"'); + process.exit(1); +} + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +const question = (query) => new Promise((resolve) => rl.question(query, resolve)); + +function sanitizeFilename(filename) { + const basename = filename.replace(/\.md$/, ''); + + return basename + .replace(/[<>:"/\\|?*.,\s]+/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/-{2,}/g, '-'); +} + +async function checkFileExists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +async function findAvailableFilename(basePath, baseName) { + let counter = 1; + let filePath = basePath; + + while (await checkFileExists(filePath)) { + if (isDir) { + const dirName = `${baseName}-${counter}`; + filePath = path.join(path.dirname(path.dirname(basePath)), dirName, 'index.md'); + } else { + filePath = path.join(path.dirname(basePath), `${baseName}-${counter}.md`); + } + counter++; + } + + return filePath; +} + +async function handleExistingFile(targetPath) { + console.log('\nFile already exists:', targetPath); + console.log('1. Overwrite'); + console.log('2. Use a different name (auto-numbered)'); + console.log('3. Cancel\n'); + + const answer = await question('Choose an option: '); + + switch (answer.trim()) { + case '1': + return targetPath; + case '2': + return await findAvailableFilename( + targetPath, + path.basename(isDir ? path.dirname(targetPath) : targetPath, '.md') + ); + case '3': + rl.close(); + console.log('\nOperation cancelled'); + process.exit(0); + break; + default: + console.log('\nInvalid option, operation cancelled'); + rl.close(); + process.exit(1); + } +} + +async function main() { + try { + const templatePath = path.resolve('scaffolds', `${type}.md`); + const template = await fs.readFile(templatePath, 'utf-8'); + + const now = new Date().toISOString(); + const variables = { + title: name, + date: now, + }; + + const content = template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => { + return variables[key] || ''; + }); + + const targetDir = path.resolve('src', 'content', `${type}s`); + const sanitizedName = sanitizeFilename(name); + let targetPath; + + if (isDir) { + targetPath = path.join(targetDir, sanitizedName, 'index.md'); + } else { + targetPath = path.join(targetDir, `${sanitizedName}.md`); + } + + if (await checkFileExists(targetPath)) { + targetPath = await handleExistingFile(targetPath); + } + + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + + await fs.writeFile(targetPath, content, 'utf-8'); + console.log(`\nSuccessfully created ${type}: ${targetPath}`); + rl.close(); + } catch (error) { + console.error('Error:', error.message); + rl.close(); + process.exit(1); + } +} + +main(); diff --git a/scripts/pub.mjs b/scripts/pub.mjs new file mode 100644 index 0000000..a886bf3 --- /dev/null +++ b/scripts/pub.mjs @@ -0,0 +1,143 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises'; +import path from 'node:path'; +import readline from 'node:readline'; + +const [, , name] = process.argv; + +if (!name) { + console.error('Usage: pnpm publish [name]'); + process.exit(1); +} + +function sanitizeFilename(filename) { + const basename = filename.replace(/\.md$/, ''); + return basename + .replace(/[<>:"/\\|?*.,\s]+/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/-{2,}/g, '-'); +} + +async function checkFileExists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +async function findDraftPath(draftDir, sanitizedName) { + // 检查常规文件 + const regularPath = path.join(draftDir, `${sanitizedName}.md`); + const dirPath = path.join(draftDir, sanitizedName, 'index.md'); + + // 记录找到的所有匹配路径 + const foundPaths = []; + + if (await checkFileExists(regularPath)) { + foundPaths.push(regularPath); + } + if (await checkFileExists(dirPath)) { + foundPaths.push(dirPath); + } + + if (foundPaths.length === 0) { + return null; + } + + if (foundPaths.length === 1) { + return foundPaths[0]; + } + + console.log('\nMultiple drafts found with the same name:'); + foundPaths.forEach((p, i) => { + console.log(`${i + 1}. ${p}`); + }); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const answer = await new Promise((resolve) => { + rl.question('\nChoose which draft to publish (enter number): ', resolve); + }); + rl.close(); + + const choice = parseInt(answer.trim()) - 1; + if (choice >= 0 && choice < foundPaths.length) { + return foundPaths[choice]; + } + + console.error('\nInvalid choice'); + process.exit(1); +} + +async function copyDirectory(src, dest) { + await fs.mkdir(dest, { recursive: true }); + const entries = await fs.readdir(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + await copyDirectory(srcPath, destPath); + } else { + await fs.copyFile(srcPath, destPath); + } + } +} + +async function main() { + try { + const sanitizedName = sanitizeFilename(name); + const draftDir = path.resolve('src', 'content', 'drafts'); + const postsDir = path.resolve('src', 'content', 'posts'); + + const draftPath = await findDraftPath(draftDir, sanitizedName); + if (!draftPath) { + console.error(`\nError: Draft not found: ${sanitizedName}`); + process.exit(1); + } + + const isDirDraft = path.basename(draftPath) === 'index.md'; + let targetPath; + + if (isDirDraft) { + targetPath = path.join(postsDir, path.basename(path.dirname(draftPath)), 'index.md'); + } else { + targetPath = path.join(postsDir, `${sanitizedName}.md`); + } + + const content = await fs.readFile(draftPath, 'utf-8'); + + const now = new Date().toISOString(); + const updatedContent = content.replace(/^(---\n(?:.*\n)*?)(---)/, (match, front, end) => { + if (!front.includes('published:')) { + return `${front}published: ${now}\n${end}`; + } + return match; + }); + + if (isDirDraft) { + const srcDir = path.dirname(draftPath); + const destDir = path.dirname(targetPath); + await copyDirectory(srcDir, destDir); + await fs.writeFile(targetPath, updatedContent, 'utf-8'); + await fs.rm(srcDir, { recursive: true }); + } else { + await fs.mkdir(postsDir, { recursive: true }); + await fs.writeFile(targetPath, updatedContent, 'utf-8'); + await fs.unlink(draftPath); + } + + console.log(`\nSuccessfully published: ${targetPath}`); + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +} + +main();