initial commit
This commit is contained in:
27
lib/date.ts
Normal file
27
lib/date.ts
Normal 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
87
lib/post-process.ts
Normal 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
73
lib/rss.tsx
Normal 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
23
lib/toc.ts
Normal 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
58
lib/utils.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user