2022-03-21 00:53:16 +08:00
|
|
|
import url from "url";
|
|
|
|
import path from "path";
|
|
|
|
import fs from "fs-extra";
|
2022-12-25 00:27:06 +08:00
|
|
|
import autocorrect from "autocorrect-node";
|
2022-03-21 00:53:16 +08:00
|
|
|
|
2022-12-03 09:43:14 +08:00
|
|
|
import YAML from 'js-yaml';
|
|
|
|
import metadataParser from 'markdown-yaml-metadata-parser';
|
2022-03-21 00:53:16 +08:00
|
|
|
|
2023-01-03 16:41:14 +08:00
|
|
|
import { renderMdx } from "./mdx.js";
|
2023-01-13 20:22:33 +08:00
|
|
|
import moment from "moment";
|
2024-03-15 15:50:04 +08:00
|
|
|
import { Icon } from "./icon.js";
|
2022-03-21 00:53:16 +08:00
|
|
|
|
|
|
|
const PUBLIC_DIR = "public";
|
|
|
|
|
|
|
|
const PEOPLE_DIR = "people";
|
2022-04-09 05:38:52 +08:00
|
|
|
const COMMENTS_DIR = "comments";
|
2022-03-21 00:53:16 +08:00
|
|
|
|
|
|
|
const DIST_DIR = "dist";
|
|
|
|
|
2024-03-17 15:57:18 +08:00
|
|
|
const DATA_DIR = "data";
|
|
|
|
|
2022-03-21 00:53:16 +08:00
|
|
|
const projectRoot = path.dirname(path.dirname(url.fileURLToPath(import.meta.url)));
|
|
|
|
const peopleDir = path.join(projectRoot, PEOPLE_DIR);
|
|
|
|
const people = fs.readdirSync(peopleDir).map(person => ({
|
|
|
|
dirname: person,
|
|
|
|
srcPath: path.join(peopleDir, person),
|
|
|
|
distPath: path.join(projectRoot, DIST_DIR, PEOPLE_DIR, person)
|
|
|
|
}));
|
|
|
|
|
2024-04-04 19:33:47 +08:00
|
|
|
interface HData {
|
2024-04-04 19:47:23 +08:00
|
|
|
commentOnly: string[]
|
|
|
|
exclude: string[]
|
2024-04-04 19:33:47 +08:00
|
|
|
notShowOnHome: string[]
|
|
|
|
}
|
|
|
|
|
|
|
|
const hdata = JSON.parse(fs.readFileSync(path.join(projectRoot, DATA_DIR, "hdata.json")).toString()) as HData;
|
|
|
|
const commentOnlyList = hdata.commentOnly;
|
2024-04-04 19:47:23 +08:00
|
|
|
const excludeList = commentOnlyList.concat(hdata.exclude);
|
2024-04-04 19:33:47 +08:00
|
|
|
const notShowOnHomeList = hdata.notShowOnHome;
|
2024-03-17 15:57:18 +08:00
|
|
|
|
2023-01-03 16:41:14 +08:00
|
|
|
interface PeopleMeta {
|
|
|
|
id: string
|
|
|
|
name: string
|
|
|
|
profileUrl: string
|
|
|
|
path: string
|
|
|
|
sortKey: string
|
|
|
|
}
|
|
|
|
|
2022-03-21 00:53:16 +08:00
|
|
|
// Transform `info.json5` to `info.json`.
|
|
|
|
// Extract metadata from `people/${dirname}/info.json5` to `dist/people-list.json`.
|
|
|
|
function buildPeopleInfoAndList() {
|
2022-12-03 09:43:14 +08:00
|
|
|
// Read internationalized key names
|
2023-01-03 16:41:14 +08:00
|
|
|
const infoKeys = YAML.load(fs.readFileSync('info-i18n.yml').toString())
|
2022-12-03 09:43:14 +08:00
|
|
|
|
|
|
|
// Compile into multiple languages
|
2023-03-14 17:23:55 +08:00
|
|
|
for (const lang of ['', '.zh_hant', '.en']) {
|
2022-12-03 09:43:14 +08:00
|
|
|
|
|
|
|
// Compiled meta of list of people for the front page (contains keys id, name, profileUrl)
|
|
|
|
const peopleList: PeopleMeta[] = [];
|
2024-04-04 19:47:23 +08:00
|
|
|
const peopleHomeList: PeopleMeta[] = [];
|
2022-12-03 09:43:14 +08:00
|
|
|
|
|
|
|
// For each person
|
|
|
|
for (const { dirname, srcPath, distPath } of people) {
|
2024-03-15 15:55:17 +08:00
|
|
|
|
2024-03-17 15:57:18 +08:00
|
|
|
if (excludeList.includes(dirname)) continue;
|
2024-03-15 15:55:17 +08:00
|
|
|
|
2022-12-03 09:59:51 +08:00
|
|
|
const infoFile = fs.readFileSync(path.join(srcPath, `info.yml`), "utf-8");
|
2023-01-03 16:41:14 +08:00
|
|
|
const info: any = YAML.load(infoFile);
|
2022-11-17 14:15:59 +08:00
|
|
|
|
2022-12-03 09:43:14 +08:00
|
|
|
// Read the page.md of that language
|
2024-01-04 11:19:21 +08:00
|
|
|
const markdown = fs.readFileSync(path.join(srcPath, `page${lang}.md`), "utf-8").replaceAll("<!--", "{/* ").replaceAll("-->", " */}");
|
2022-11-17 14:15:59 +08:00
|
|
|
|
2022-12-03 09:43:14 +08:00
|
|
|
// Get the markdown header
|
|
|
|
const mdMeta = metadataParser(markdown).metadata
|
|
|
|
info.name = mdMeta.name
|
|
|
|
|
2022-12-03 09:59:51 +08:00
|
|
|
// Convert website dict into entries [[k, v], ...]
|
|
|
|
info.websites = Object.entries(info.websites ?? {})
|
|
|
|
|
2023-01-03 16:41:14 +08:00
|
|
|
// Get sort key
|
|
|
|
const sortKey = info.info?.died ?? mdMeta.info?.died ?? '0'
|
|
|
|
|
2023-01-13 20:22:33 +08:00
|
|
|
// Add age
|
|
|
|
if (info.info && info.info.died && info.info.born)
|
|
|
|
{
|
|
|
|
try { info.info.age = Math.abs(moment(info.info.died).diff(info.info.born, 'years', false)) }
|
|
|
|
catch (e) { console.log(`Unable to calculate age for ${dirname}`) }
|
|
|
|
}
|
|
|
|
|
2022-12-03 09:43:14 +08:00
|
|
|
// Convert info dict to [[key, value], ...]
|
|
|
|
// And add info k-v pairs from markdown to the info object in json5
|
|
|
|
info.info = [...Object.entries(mdMeta.info ?? {}), ...Object.entries(info.info ?? {})]
|
|
|
|
|
|
|
|
// Convert key names to internationalized key names
|
2023-01-03 16:41:14 +08:00
|
|
|
let langKey = trim(lang, ".")
|
2022-12-03 09:43:14 +08:00
|
|
|
if (langKey == '') langKey = "zh_hans"
|
|
|
|
const keys = infoKeys[langKey]['key']
|
|
|
|
info.info = info.info.map(pair => [pair[0] in keys ? keys[pair[0]] : pair[0], pair[1]])
|
2022-11-17 14:15:59 +08:00
|
|
|
|
|
|
|
// Combine comments in people/${dirname}/comments/${cf}.json
|
|
|
|
const commentPath = path.join(srcPath, COMMENTS_DIR)
|
|
|
|
fs.ensureDirSync(commentPath)
|
|
|
|
info.comments = fs.readdirSync(commentPath)
|
2022-04-09 05:38:52 +08:00
|
|
|
.filter(cf => cf.endsWith('.json'))
|
|
|
|
.map(cf => JSON.parse(fs.readFileSync(path.join(commentPath, cf), 'utf-8')))
|
|
|
|
|
2022-12-25 00:27:06 +08:00
|
|
|
// Autocorrect comment contents
|
|
|
|
info.comments.forEach(c => c.content = autocorrect.format(c.content))
|
|
|
|
|
2022-11-17 14:15:59 +08:00
|
|
|
// Write info.json
|
|
|
|
fs.ensureDirSync(distPath);
|
|
|
|
fs.writeFileSync(path.join(distPath, `info${lang}.json`), JSON.stringify(info));
|
2022-12-03 09:43:14 +08:00
|
|
|
|
|
|
|
// Create people list meta information
|
|
|
|
const peopleMeta = {
|
|
|
|
path: dirname,
|
2023-01-03 16:41:14 +08:00
|
|
|
sortKey: sortKey,
|
|
|
|
...Object.fromEntries(["id", "name", "profileUrl"].map(key => [key, info[key]]))
|
2022-12-03 09:43:14 +08:00
|
|
|
} as PeopleMeta;
|
|
|
|
|
|
|
|
// Add meta to people list
|
2024-04-04 19:47:23 +08:00
|
|
|
if (peopleList.filter(it => it.id == peopleMeta.id).length == 0) {
|
2022-12-03 09:43:14 +08:00
|
|
|
peopleList.push(peopleMeta);
|
2024-04-04 19:51:15 +08:00
|
|
|
if (!notShowOnHomeList.includes(peopleMeta.id))
|
2024-04-04 19:47:23 +08:00
|
|
|
peopleHomeList.push(peopleMeta)
|
|
|
|
}
|
2022-11-17 14:15:59 +08:00
|
|
|
}
|
2022-03-21 00:53:16 +08:00
|
|
|
|
2023-01-03 16:41:14 +08:00
|
|
|
peopleList.sort((a, b) => b.sortKey.localeCompare(a.sortKey))
|
2024-04-04 19:47:23 +08:00
|
|
|
peopleHomeList.sort((a, b) => b.sortKey.localeCompare(a.sortKey))
|
2023-01-03 16:41:14 +08:00
|
|
|
|
2022-12-03 09:43:14 +08:00
|
|
|
// Write people-list.json
|
|
|
|
fs.writeFileSync(path.join(projectRoot, DIST_DIR, `people-list${lang}.json`), JSON.stringify(peopleList));
|
2024-04-04 19:47:23 +08:00
|
|
|
fs.writeFileSync(path.join(projectRoot, DIST_DIR, `people-home-list${lang}.json`), JSON.stringify(peopleHomeList));
|
2022-12-03 09:43:14 +08:00
|
|
|
}
|
2022-03-21 00:53:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Render `people/${dirname}/page.md` to `dist/people/${dirname}/page.js`.
|
|
|
|
function buildPeoplePages() {
|
2023-03-14 17:23:55 +08:00
|
|
|
for (const { dirname, srcPath, distPath } of people) {
|
2024-03-17 15:57:18 +08:00
|
|
|
|
|
|
|
if (excludeList.includes(dirname)) continue;
|
|
|
|
|
2023-03-14 17:23:55 +08:00
|
|
|
for (const lang of ['', '.zh_hant', '.en'])
|
2022-11-17 14:15:59 +08:00
|
|
|
{
|
2022-12-03 09:43:14 +08:00
|
|
|
// Read markdown page and remove markdown meta
|
2024-01-04 11:19:21 +08:00
|
|
|
let markdown = metadataParser(fs.readFileSync(path.join(srcPath, `page${lang}.md`), "utf-8")).content.replaceAll("<!--", "{/* ").replaceAll("-->", " */}");
|
2022-12-25 00:27:06 +08:00
|
|
|
|
2024-02-28 22:41:06 +08:00
|
|
|
// Handle Footnote
|
|
|
|
markdown = handleFootnote(markdown)
|
|
|
|
|
2024-03-15 15:50:04 +08:00
|
|
|
// Handle Icon
|
|
|
|
markdown = handleNoteIcon(markdown)
|
|
|
|
|
2022-12-25 00:27:06 +08:00
|
|
|
// Autocorrect markdown
|
|
|
|
markdown = autocorrect.formatFor(markdown, 'markdown')
|
|
|
|
|
|
|
|
// Render mdx
|
2023-03-14 17:23:55 +08:00
|
|
|
console.log('GENERATED: '+dirname+lang)
|
2022-11-17 14:15:59 +08:00
|
|
|
const result = renderMdx(markdown);
|
2022-03-21 00:53:16 +08:00
|
|
|
|
2022-11-17 14:15:59 +08:00
|
|
|
fs.ensureDirSync(distPath);
|
2023-04-13 14:40:52 +08:00
|
|
|
fs.writeFileSync(path.join(distPath, `page${lang}.json`), JSON.stringify(result));
|
2022-11-17 14:15:59 +08:00
|
|
|
}
|
2022-03-21 00:53:16 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-28 22:41:06 +08:00
|
|
|
function handleFootnote(md: string) {
|
2024-02-29 07:56:09 +08:00
|
|
|
if (!md.includes('[^')) return md
|
|
|
|
|
|
|
|
// Replace footnote references with HTML superscript tags
|
2024-02-29 07:58:33 +08:00
|
|
|
return md.replace(/\[\^(\d+)\](?::\s*(.*))?/g, (match, id, text) => text ?
|
2024-02-29 07:56:09 +08:00
|
|
|
// Footnote definition
|
|
|
|
`<li id="footnote-${id}">${text}<a href="#footnote-ref-${id}">↩</a></li>` :
|
|
|
|
// Footnote reference
|
|
|
|
`<sup><a href="#footnote-${id}" id="footnote-ref-${id}">${id}</a></sup>`
|
|
|
|
)
|
2024-02-29 07:58:33 +08:00
|
|
|
|
2024-02-29 07:56:09 +08:00
|
|
|
// Wrap the footnote definitions in an ordered list
|
2024-02-29 07:58:33 +08:00
|
|
|
.replace(/(<li id="footnote.*<\/li>)/gs, '<ol>\n$1\n</ol>')
|
2024-02-28 22:41:06 +08:00
|
|
|
}
|
|
|
|
|
2024-03-15 15:50:04 +08:00
|
|
|
function handleNoteIcon(md: string): string {
|
|
|
|
if (!md.includes('[!')) return md;
|
|
|
|
return md.replace(/\[\!(\w+)\](?::\s*(.*))?/g, (match, icon, _) => (Icon[icon as string]));
|
|
|
|
}
|
|
|
|
|
2022-04-09 05:38:52 +08:00
|
|
|
// Copy `people/${dirname}/photos` to `dist/people/${dirname}/`.
|
2022-03-21 00:53:16 +08:00
|
|
|
function copyPeopleAssets() {
|
2023-01-05 04:52:49 +08:00
|
|
|
const PEOPLE_ASSETS = ["photos", "backup", "page.md"];
|
2022-03-21 00:53:16 +08:00
|
|
|
|
|
|
|
for (const { srcPath, distPath } of people) {
|
|
|
|
fs.ensureDirSync(distPath);
|
|
|
|
|
|
|
|
for (const assetDirname of PEOPLE_ASSETS) {
|
|
|
|
const assetSrcPath = path.join(srcPath, assetDirname);
|
|
|
|
if (fs.pathExistsSync(assetSrcPath)) {
|
|
|
|
fs.copySync(assetSrcPath, path.join(distPath, assetDirname));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy files `public` to dist.
|
|
|
|
function copyPublic() {
|
|
|
|
fs.copySync(path.join(projectRoot, PUBLIC_DIR), path.join(projectRoot, DIST_DIR));
|
|
|
|
}
|
|
|
|
|
2024-03-17 15:57:18 +08:00
|
|
|
function copyComments() {
|
|
|
|
for (const dirname of commentOnlyList) {
|
|
|
|
const commentPath = path.join(peopleDir, dirname as string, COMMENTS_DIR);
|
|
|
|
const distPath = path.join(projectRoot, DIST_DIR, PEOPLE_DIR, dirname as string);
|
|
|
|
fs.ensureDirSync(commentPath);
|
|
|
|
var info = { comments: [] };
|
|
|
|
info.comments = fs
|
|
|
|
.readdirSync(commentPath)
|
|
|
|
.filter((cf) => cf.endsWith(".json"))
|
|
|
|
.map((cf) =>
|
|
|
|
JSON.parse(fs.readFileSync(path.join(commentPath, cf), "utf-8"))
|
|
|
|
);
|
|
|
|
info.comments.forEach((c) => (c.content = autocorrect.format(c.content)));
|
|
|
|
fs.ensureDirSync(distPath);
|
|
|
|
fs.writeFileSync(path.join(distPath, "info.json"), JSON.stringify(info));
|
2024-03-31 09:30:57 +08:00
|
|
|
fs.writeFileSync(path.join(distPath, "info.en.json"), JSON.stringify(info));
|
|
|
|
fs.writeFileSync(path.join(distPath, "info.zh_hant.json"), JSON.stringify(info));
|
2024-03-17 15:57:18 +08:00
|
|
|
}
|
2024-03-15 16:14:19 +08:00
|
|
|
}
|
|
|
|
|
2022-03-21 00:53:16 +08:00
|
|
|
buildPeopleInfoAndList();
|
|
|
|
buildPeoplePages();
|
|
|
|
copyPeopleAssets();
|
|
|
|
copyPublic();
|
2024-03-17 15:57:18 +08:00
|
|
|
copyComments();
|
2022-12-03 09:43:14 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Trim a specific char from a string
|
|
|
|
*
|
|
|
|
* @param str String
|
|
|
|
* @param ch Character (must have len 1)
|
|
|
|
*/
|
2023-01-03 16:41:14 +08:00
|
|
|
function trim(str: string, ch: string) {
|
2022-12-03 09:43:14 +08:00
|
|
|
let start = 0
|
|
|
|
let end = str.length
|
|
|
|
|
|
|
|
while (start < end && str[start] === ch)
|
|
|
|
++start;
|
|
|
|
|
|
|
|
while (end > start && str[end - 1] === ch)
|
|
|
|
--end;
|
|
|
|
|
|
|
|
return (start > 0 || end < str.length) ? str.substring(start, end) : str;
|
|
|
|
}
|