initial commit

This commit is contained in:
PrinOrange
2023-12-25 17:21:39 +08:00
commit 0bd1089d74
94 changed files with 18648 additions and 0 deletions

27
lib/date.ts Normal file
View File

@@ -0,0 +1,27 @@
/**
* Convert the date format of YYYY-MM-DD to American writing
* @param date The date in format of YYYY-MM-DD.
*/
export const normalizeDate = (date: string): string => {
if (date == null) return "01 January, 1970";
let [year, month, day] = date.split("-");
let month_num = parseInt(month);
let day_num = parseInt(day);
const month_en: {
[index: number]: string;
} = {
1: "January",
2: "February",
3: "March",
4: "April",
5: "May",
6: "June",
7: "July",
8: "August",
9: "September",
10: "October",
11: "November",
12: "December",
};
return `${day_num} ${month_en[month_num]}, ${year}`;
};

87
lib/post-process.ts Normal file
View File

@@ -0,0 +1,87 @@
import { PostsRootDirectory } from "@/consts/consts";
import { TFrontmatter } from "@/types/frontmatter.type";
import { TPostListItem, TTagSubPostSet } from "@/types/post-list";
import fs from "fs";
import { serialize } from "next-mdx-remote/serialize";
import path from "path";
import { nullifyEmptyString } from "./utils";
async function getFrontmatters(filepath: string): Promise<TFrontmatter> {
const source = fs.readFileSync(filepath, "utf-8");
const mdxSource = await serialize(source, {
parseFrontmatter: true,
mdxOptions: { format: "md" },
});
return mdxSource.frontmatter as TFrontmatter;
}
export const getPostFileContent = (postId: string): string | null => {
const filePath = path.join(PostsRootDirectory, `${postId}.md`);
if (!fs.existsSync(filePath)) return null;
const source = fs.readFileSync(filePath, "utf-8");
return source;
};
const sortOutPostLists = async (): Promise<{
allPostList: TPostListItem[];
pinnedPostList: TPostListItem[];
tagSubPostSet: TTagSubPostSet;
}> => {
const allPostListItems: TPostListItem[] = [];
const pinnedPostList: TPostListItem[] = [];
const tagSubPostSet: TTagSubPostSet = {};
const postFilePaths: string[] = [];
fs.readdirSync(PostsRootDirectory).forEach((fileName) => {
const filePath = path.join(PostsRootDirectory, fileName);
const fileStat = fs.statSync(filePath);
if (fileStat.isFile() && fileName.endsWith(".md")) {
postFilePaths.push(filePath);
}
});
for (let i = 0; i < postFilePaths.length; i++) {
const frontmatter = await getFrontmatters(postFilePaths[i]);
const postId = path.parse(postFilePaths[i]).name;
const postListItem: TPostListItem = {
id: postId,
frontMatter: {
title: frontmatter.title,
subtitle: nullifyEmptyString(frontmatter.subtitle),
coverURL: nullifyEmptyString(frontmatter.coverURL),
tags: frontmatter.tags ?? [],
summary: nullifyEmptyString(frontmatter.summary),
time: frontmatter.time,
pin: frontmatter.pin ?? false,
noPrompt: frontmatter.noPrompt ?? false,
allowShare: frontmatter.allowShare ?? true,
},
};
allPostListItems.push(postListItem);
if (postListItem.frontMatter.pin) {
pinnedPostList.push(postListItem);
}
}
allPostListItems.sort((a, b) => {
return a.frontMatter.time > b.frontMatter.time ? -1 : 1;
});
allPostListItems.map((item) => {
item.frontMatter.tags?.map((tagName) => {
if (tagSubPostSet[tagName] == null) {
tagSubPostSet[tagName] = [];
}
tagSubPostSet[tagName].push(item);
});
});
return { allPostList: allPostListItems, tagSubPostSet: tagSubPostSet, pinnedPostList: pinnedPostList };
};
export const sortedPosts = await sortOutPostLists();

73
lib/rss.tsx Normal file
View File

@@ -0,0 +1,73 @@
import { LatestPostCountInHomePage } from "@/consts/consts";
import { Config } from "@/data/config";
import { Feed } from "feed";
import fs from "fs";
import { MDXRemote } from "next-mdx-remote";
import { serialize } from "next-mdx-remote/serialize";
import { renderToString } from "react-dom/server";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeMathJax from "rehype-mathjax/svg";
import rehypePresetMinify from "rehype-preset-minify";
import rehypeSlug from "rehype-slug";
import externalLinks from "remark-external-links";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import remarkPrism from "remark-prism";
import { getPostFileContent, sortedPosts } from "./post-process";
/**
* Generate the RSS Feed File in `./public` so it could be visited by https://domain/rss.xml
*/
export const generateRSSFeed = async () => {
const feed = new Feed({
title: Config.SiteTitle,
description: Config.Sentence,
id: Config.SiteDomain,
link: `https://${Config.SiteDomain}/`,
image: Config.PageCovers.websiteCoverURL,
favicon: `https://${Config.SiteDomain}/favcion.ico`,
copyright: `COPYRIGHT © ${Config.YearStart}-${new Date().getFullYear()} ${Config.AuthorName} ALL RIGHTS RESERVED`,
generator: "Node.js Feed",
author: {
name: Config.AuthorName,
email: Config.SocialLinks.email,
link: `https://${Config.SiteDomain}/`,
},
});
for (let i = 0; i < LatestPostCountInHomePage; i++) {
const post = sortedPosts.allPostList[i];
const postContent = `${getPostFileContent(
post.id,
)}\n---\n**NOTE:** Different RSS reader may have deficient even no support for svg formulations rendering. If it happens, please read the origin page to have better experience.`;
const dateNumber = post.frontMatter.time.split("-").map((num) => parseInt(num));
const mdxSource = await serialize(postContent ?? "", {
parseFrontmatter: true,
mdxOptions: {
remarkPlugins: [remarkPrism, externalLinks, remarkMath, remarkGfm],
rehypePlugins: [rehypeMathJax, rehypeAutolinkHeadings, rehypeSlug, rehypePresetMinify as any],
format: "md",
},
});
const htmlContent = renderToString(<MDXRemote {...mdxSource} />);
feed.addItem({
title: post.frontMatter.title,
id: post.id,
link: `https://${Config.SiteDomain}/blog/${post.id}`,
description: post.frontMatter.summary ?? undefined,
content: htmlContent,
author: [
{
name: Config.AuthorName,
email: Config.SocialLinks.email,
link: `https://${Config.SiteDomain}/`,
},
],
category: post.frontMatter.tags?.map((tagname) => ({ name: tagname })),
date: new Date(dateNumber[0], dateNumber[1], dateNumber[2]),
image: post.frontMatter.coverURL ?? undefined,
});
}
fs.writeFile("./public/rss.xml", feed.rss2(), "utf-8", (err) => {});
};

23
lib/toc.ts Normal file
View File

@@ -0,0 +1,23 @@
import { TTOCItem } from "@/types/toc.type";
import { JSDOM } from "jsdom";
/**
* Generate the Table Of Content List by html code.
* It supports h1-h6 level headings at most.
* @param htmlCode
* @returns
*/
export const getTOCTree = (htmlCode: string) => {
const doc_dom = new JSDOM(htmlCode);
const all_headers = doc_dom.window.document.querySelectorAll("h1,h2,h3,h4,h5,h6");
const result: TTOCItem[] = [];
for (let i = 0; i < all_headers.length; i++) {
const level = parseInt(all_headers[i].tagName.replace("H", ""));
result.push({
level: level,
anchorId: all_headers[i].id,
title: all_headers[i].textContent!,
});
}
return result;
};

58
lib/utils.ts Normal file
View File

@@ -0,0 +1,58 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* The implementation for pagination queries in an array.
* @param array Array input
* @param pageSize Max elements count for every page.
* @param pageNumber The current page number.
* @returns A new array contains elements of specified page.
*/
export function paginateArray<T = any>(array: T[], pageSize: number, pageNumber: number) {
if (array.length === 0) return [];
if (pageSize < 1) return [];
if (pageNumber < 1) return [];
--pageNumber;
const startIndex = pageNumber * pageSize;
const endIndex = (pageNumber + 1) * pageSize;
if (startIndex >= array.length) return [];
return array.slice(startIndex, endIndex);
}
/**
* Nullify the empty string and convert them into `null`.
* @param value Possible string input.
* @returns return `null` if the input belongs to "", undefined and null.
*/
export function nullifyEmptyString(value: string | null | undefined): string | null {
if (value == null) {
return null;
}
if (value.trim() === "") {
return null;
}
return value;
}
/**
* Indicate whether a string is empty value.
* @param value Possible string input.
* @returns return `true` if the input belongs to "", undefined and null.
*/
export function isEmptyString(value: string | null | undefined): boolean {
if (value == null) {
return true;
}
if (value.trim() === "") {
return true;
}
return false;
}