[fix] upgrade the frontmatter stringify-handling functions
This commit is contained in:
38
lib/frontmatter.ts
Normal file
38
lib/frontmatter.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Checks if the object contains only valid types.
|
||||||
|
*/
|
||||||
|
function isValidFrontmatterObject(obj: Record<string, any>): boolean {
|
||||||
|
return Object.values(obj).every(
|
||||||
|
(value) =>
|
||||||
|
value == null || // null or undefined
|
||||||
|
typeof value === "string" ||
|
||||||
|
typeof value === "boolean" ||
|
||||||
|
typeof value === "number" ||
|
||||||
|
(Array.isArray(value) && value.every((item) => typeof item === "string")), // valid string array
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a valid object into YAML-like frontmatter string.
|
||||||
|
* Throws error if object contains invalid types.
|
||||||
|
*/
|
||||||
|
export function stringifyFrontmatter(obj: Record<string, any>): string {
|
||||||
|
if (!isValidFrontmatterObject(obj)) {
|
||||||
|
throw new Error("Invalid frontmatter");
|
||||||
|
}
|
||||||
|
|
||||||
|
return `---\n${Object.entries(obj)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
if (value == null) {
|
||||||
|
return `${key}: null`; // Handle null values explicitly
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return `${key}: ${JSON.stringify(value)}`; // Handle arrays
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return `${key}: ${JSON.stringify(value)}`; // Wrap strings with quotes
|
||||||
|
}
|
||||||
|
return `${key}: ${value}`; // Handle numbers and booleans
|
||||||
|
})
|
||||||
|
.join("\n")}\n---`;
|
||||||
|
}
|
||||||
@@ -1,60 +1,60 @@
|
|||||||
import { type ChildProcessWithoutNullStreams, type SpawnSyncReturns, spawn, spawnSync } from "child_process";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { PostFilesDirectory } from "@/consts/consts";
|
import { PostFilesDirectory } from "@/consts/consts";
|
||||||
import { getCurrentTime } from "@/lib/date";
|
import { getCurrentTime } from "@/lib/date";
|
||||||
|
import { stringifyFrontmatter } from "@/lib/frontmatter";
|
||||||
|
import { type ChildProcessWithoutNullStreams, type SpawnSyncReturns, spawn, spawnSync } from "child_process";
|
||||||
import colors from "colors";
|
import colors from "colors";
|
||||||
|
import fs from "fs";
|
||||||
import inquirer, { type QuestionCollection } from "inquirer";
|
import inquirer, { type QuestionCollection } from "inquirer";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import path from "path";
|
||||||
import { titleCase } from "title-case";
|
import { titleCase } from "title-case";
|
||||||
|
|
||||||
type TAnswer = {
|
type TAnswer = {
|
||||||
title: string;
|
inputTitle: string;
|
||||||
subtitle: string;
|
inputSubtitle: string;
|
||||||
tags: string;
|
inputTags: string;
|
||||||
noPrompt: boolean;
|
inputNoPrompt: boolean;
|
||||||
pin: boolean;
|
inputPin: boolean;
|
||||||
allowShare: boolean;
|
inputAllowShare: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const questions: QuestionCollection<TAnswer> = [
|
const questions: QuestionCollection<TAnswer> = [
|
||||||
{
|
{
|
||||||
type: "input",
|
type: "input",
|
||||||
name: "title",
|
name: "inputTitle",
|
||||||
message: "What's the title?",
|
message: "What's the title? (Required)",
|
||||||
validate: (input: string) => (input.trim() === "" ? "Please enter a title." : true),
|
validate: (input: string) => (input.trim() === "" ? "Please enter a title." : true),
|
||||||
},
|
},
|
||||||
{ type: "input", name: "subtitle", message: "What's the subtitle?" },
|
{
|
||||||
{ type: "input", name: "tags", message: "Assign tags for the posts and separate them with commas." },
|
type: "input",
|
||||||
{ type: "confirm", name: "noPrompt", message: "Do NOT prompt this post? (D:false)", default: false },
|
name: "inputSubtitle",
|
||||||
{ type: "confirm", name: "pin", message: "Do you want to pin this post? (D:false)", default: false },
|
message: "What's the subtitle? (Optional)",
|
||||||
{ type: "confirm", name: "allowShare", message: "Do you allow everybody share this post? (D:true)", default: true },
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "inputTags",
|
||||||
|
message: "Assign tags for the posts and separate them with commas. (Required, default: others)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "confirm",
|
||||||
|
name: "inputNoPrompt",
|
||||||
|
message: "Do NOT prompt this post? (Required, Default:false)",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "confirm",
|
||||||
|
name: "inputPin",
|
||||||
|
message: "Do you want to pin this post? (Required, Default:false)",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "confirm",
|
||||||
|
name: "inputAllowShare",
|
||||||
|
message: "Do you allow everybody share this post? (Required, Default:true)",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const createTemplate = (answers: TAnswer): string => {
|
|
||||||
const { year, month, day } = getCurrentTime();
|
|
||||||
const tags = JSON.stringify(
|
|
||||||
answers.tags
|
|
||||||
.split(",")
|
|
||||||
.map((tag) => tag.trim())
|
|
||||||
.filter((tag) => tag !== ""),
|
|
||||||
);
|
|
||||||
|
|
||||||
return `---
|
|
||||||
title: "${titleCase(answers.title)}"
|
|
||||||
subtitle: "${answers.subtitle}"
|
|
||||||
summary: ""
|
|
||||||
author: ""
|
|
||||||
coverURL: ""
|
|
||||||
time: "${year}-${month}-${day}"
|
|
||||||
tags: ${tags}
|
|
||||||
noPrompt: ${answers.noPrompt}
|
|
||||||
pin: ${answers.pin}
|
|
||||||
allowShare: ${answers.allowShare}
|
|
||||||
closed: false
|
|
||||||
---`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const writePostFile = (filePath: string, content: string): void => {
|
const writePostFile = (filePath: string, content: string): void => {
|
||||||
fs.writeFile(filePath, content, "utf-8", (err) => {
|
fs.writeFile(filePath, content, "utf-8", (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -65,28 +65,61 @@ const writePostFile = (filePath: string, content: string): void => {
|
|||||||
console.log(colors.green(colors.bold("Create Post Succeed.")));
|
console.log(colors.green(colors.bold("Create Post Succeed.")));
|
||||||
console.log(`Open the file ${colors.cyan(filePath)} to write your blog now.`);
|
console.log(`Open the file ${colors.cyan(filePath)} to write your blog now.`);
|
||||||
console.log("Some fields, such as summary, need to be filled in by yourself after opening the file.");
|
console.log("Some fields, such as summary, need to be filled in by yourself after opening the file.");
|
||||||
|
|
||||||
const openFileCommand: Record<string, () => ChildProcessWithoutNullStreams | SpawnSyncReturns<Buffer>> = {
|
|
||||||
win32: () => spawn("cmd", ["/c", "start", filePath]),
|
|
||||||
darwin: () => spawnSync("open", [filePath]),
|
|
||||||
linux: () => spawnSync("xdg-open", [filePath]),
|
|
||||||
freebsd: () => spawnSync("xdg-open", [filePath]),
|
|
||||||
};
|
|
||||||
|
|
||||||
const command = openFileCommand[process.platform];
|
|
||||||
if (command) command();
|
|
||||||
else {
|
|
||||||
console.log("Unsupported platform. Open the file manually please.");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openPostFile = (filePath: string) => {
|
||||||
|
const openFileCommand: Record<string, () => ChildProcessWithoutNullStreams | SpawnSyncReturns<Buffer>> = {
|
||||||
|
win32: () => spawn("cmd", ["/c", "start", filePath]),
|
||||||
|
darwin: () => spawnSync("open", [filePath]),
|
||||||
|
linux: () => spawnSync("xdg-open", [filePath]),
|
||||||
|
freebsd: () => spawnSync("xdg-open", [filePath]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const command = openFileCommand[process.platform];
|
||||||
|
if (command) command();
|
||||||
|
else {
|
||||||
|
console.log("Unsupported platform. Open the file manually please.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
inquirer.prompt<TAnswer>(questions).then((answers) => {
|
inquirer.prompt<TAnswer>(questions).then((answers) => {
|
||||||
|
// Process the answer
|
||||||
const { year, month, day } = getCurrentTime();
|
const { year, month, day } = getCurrentTime();
|
||||||
const content = createTemplate(answers);
|
const title = titleCase(answers.inputTitle);
|
||||||
const sluggedTitle = _.kebabCase(answers.title);
|
const subtitle = titleCase(answers.inputSubtitle);
|
||||||
const postFileName = `${year}-${month}-${day}-${sluggedTitle}.md`;
|
|
||||||
|
const tags = _.uniq(
|
||||||
|
answers.inputTags
|
||||||
|
.split(",")
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.filter((tag) => tag !== ""),
|
||||||
|
);
|
||||||
|
if (tags.length === 0) tags.push("others");
|
||||||
|
|
||||||
|
const pin = answers.inputPin;
|
||||||
|
const noPrompt = answers.inputNoPrompt;
|
||||||
|
const allowShare = answers.inputAllowShare;
|
||||||
|
const time = `${year}-${month}-${day}`;
|
||||||
|
|
||||||
|
// Stringify the frontmatters
|
||||||
|
const stringifiedFrontmatter = stringifyFrontmatter({
|
||||||
|
title: title,
|
||||||
|
subtitle: subtitle,
|
||||||
|
summary: "",
|
||||||
|
coverURL: null,
|
||||||
|
time: time,
|
||||||
|
tags: tags,
|
||||||
|
pin: pin,
|
||||||
|
noPrompt: noPrompt,
|
||||||
|
allowShare: allowShare,
|
||||||
|
closed: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Output the new post file
|
||||||
|
const postFileName = `${year}-${month}-${day}-${_.kebabCase(answers.inputTitle)}.md`;
|
||||||
const postFilePath = path.resolve(path.join(PostFilesDirectory, postFileName));
|
const postFilePath = path.resolve(path.join(PostFilesDirectory, postFileName));
|
||||||
|
|
||||||
writePostFile(postFilePath, content);
|
writePostFile(postFilePath, `${stringifiedFrontmatter}\n`);
|
||||||
|
openPostFile(postFilePath);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user