自动化
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { relative, resolve } from "node:path";
|
||||
|
||||
const peoplePath = resolve(process.env.PEOPLE_PATH ?? "src/data/people.ts");
|
||||
const issueBody = process.env.ISSUE_BODY ?? "";
|
||||
|
||||
const fields = [
|
||||
"slug",
|
||||
"initial",
|
||||
"name",
|
||||
"photo",
|
||||
"location",
|
||||
"school",
|
||||
"direction",
|
||||
"keywords",
|
||||
"text",
|
||||
"currentStatus",
|
||||
"highlight",
|
||||
"toPastSelf",
|
||||
"messageToClass",
|
||||
"favoriteMemory",
|
||||
"contact"
|
||||
];
|
||||
|
||||
const requiredFields = [
|
||||
"slug",
|
||||
"initial",
|
||||
"name",
|
||||
"location",
|
||||
"school",
|
||||
"direction",
|
||||
"keywords",
|
||||
"text",
|
||||
"currentStatus",
|
||||
"highlight",
|
||||
"toPastSelf",
|
||||
"messageToClass",
|
||||
"favoriteMemory"
|
||||
];
|
||||
|
||||
const sectionAliases = new Map(fields.map((field) => [field.toLowerCase(), field]));
|
||||
|
||||
function normalizeValue(value) {
|
||||
const trimmed = value.trim();
|
||||
return trimmed === "_No response_" ? "" : trimmed;
|
||||
}
|
||||
|
||||
function parseIssueForm(body) {
|
||||
const result = {};
|
||||
const sectionPattern = /^###\s+(.+?)\s*$/gm;
|
||||
const matches = [...body.matchAll(sectionPattern)];
|
||||
|
||||
for (let index = 0; index < matches.length; index += 1) {
|
||||
const rawLabel = matches[index][1].trim().toLowerCase();
|
||||
const field = sectionAliases.get(rawLabel);
|
||||
if (!field) continue;
|
||||
|
||||
const start = matches[index].index + matches[index][0].length;
|
||||
const end = index + 1 < matches.length ? matches[index + 1].index : body.length;
|
||||
result[field] = normalizeValue(body.slice(start, end));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function assertValidInput(input) {
|
||||
for (const field of requiredFields) {
|
||||
if (!input[field]) {
|
||||
throw new Error(`Issue form is missing required field: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.slug)) {
|
||||
throw new Error("slug must use lowercase letters, numbers, and single hyphens only.");
|
||||
}
|
||||
}
|
||||
|
||||
function readPeopleModule(filePath) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const executable = source
|
||||
.replace("export const peopleIntro =", "const peopleIntro =")
|
||||
.replace("export const people =", "const people =");
|
||||
|
||||
return Function(`${executable}; return { peopleIntro, people };`)();
|
||||
}
|
||||
|
||||
function splitKeywords(value) {
|
||||
return value
|
||||
.split(/[、,,]/)
|
||||
.map((keyword) => keyword.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildPerson(input, existing = {}) {
|
||||
const person = { ...existing };
|
||||
|
||||
for (const field of fields) {
|
||||
if (field === "keywords") continue;
|
||||
if (input[field] || !(field in person)) {
|
||||
person[field] = input[field] ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
if (input.keywords) {
|
||||
person.keywords = splitKeywords(input.keywords);
|
||||
} else if (!Array.isArray(person.keywords)) {
|
||||
person.keywords = [];
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
function quote(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function formatPerson(person) {
|
||||
return ` {
|
||||
slug: ${quote(person.slug)},
|
||||
initial: ${quote(person.initial)},
|
||||
name: ${quote(person.name)},
|
||||
photo: ${quote(person.photo ?? "")},
|
||||
location: ${quote(person.location)},
|
||||
school: ${quote(person.school)},
|
||||
direction: ${quote(person.direction)},
|
||||
keywords: [${person.keywords.map(quote).join(", ")}],
|
||||
text: ${quote(person.text)},
|
||||
currentStatus: ${quote(person.currentStatus)},
|
||||
highlight: ${quote(person.highlight)},
|
||||
toPastSelf: ${quote(person.toPastSelf)},
|
||||
messageToClass: ${quote(person.messageToClass)},
|
||||
favoriteMemory: ${quote(person.favoriteMemory)},
|
||||
contact: ${quote(person.contact ?? "")}
|
||||
}`;
|
||||
}
|
||||
|
||||
function formatPeopleModule(peopleIntro, people) {
|
||||
return `export const peopleIntro = {
|
||||
title: ${quote(peopleIntro.title)},
|
||||
text: ${quote(peopleIntro.text)}
|
||||
};
|
||||
|
||||
export const people = [
|
||||
${people.map(formatPerson).join(",\n")}
|
||||
];
|
||||
`;
|
||||
}
|
||||
|
||||
const input = parseIssueForm(issueBody);
|
||||
assertValidInput(input);
|
||||
|
||||
const { peopleIntro, people } = readPeopleModule(peoplePath);
|
||||
const existingIndex = people.findIndex((person) => person.slug === input.slug);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
people[existingIndex] = buildPerson(input, people[existingIndex]);
|
||||
console.log(`Updated existing person: ${input.slug}`);
|
||||
} else {
|
||||
people.push(buildPerson(input));
|
||||
console.log(`Added new person: ${input.slug}`);
|
||||
}
|
||||
|
||||
writeFileSync(peoplePath, formatPeopleModule(peopleIntro, people), "utf8");
|
||||
console.log(`Wrote ${relative(process.cwd(), peoplePath)}`);
|
||||
Reference in New Issue
Block a user