101 lines
4.0 KiB
JavaScript
101 lines
4.0 KiB
JavaScript
import fs from "node:fs/promises";
|
|
|
|
const requiredFields = [
|
|
"id",
|
|
"title",
|
|
"en",
|
|
"department",
|
|
"category",
|
|
"date",
|
|
"lead",
|
|
"clearance",
|
|
"abstract",
|
|
"source",
|
|
];
|
|
const isText = (value) => typeof value === "string" && value.trim().length > 0;
|
|
|
|
export function validateContent(content) {
|
|
const errors = [];
|
|
if (!content || typeof content !== "object" || Array.isArray(content)) {
|
|
throw new Error("档案数据必须是 JSON 对象。");
|
|
}
|
|
for (const key of ["categories", "columns"]) {
|
|
const names = content[key];
|
|
if (!Array.isArray(names) || names.length !== 5 || !names.every(isText)) {
|
|
errors.push(`${key}:必须包含五个非空分类名称`);
|
|
} else if (new Set(names).size !== 5 || names.includes("全部档案")) {
|
|
errors.push(`${key}:分类名称不能重复,也不能使用“全部档案”`);
|
|
}
|
|
}
|
|
const categories = Array.isArray(content.categories)
|
|
? content.categories
|
|
: [];
|
|
const columns = Array.isArray(content.columns) ? content.columns : [];
|
|
if (
|
|
categories.some((name) => !columns.includes(name)) ||
|
|
columns.some((name) => !categories.includes(name))
|
|
) {
|
|
errors.push("categories 与 columns 必须包含相同的五个分类(顺序可以不同)");
|
|
}
|
|
const records = Array.isArray(content.records) ? content.records : [];
|
|
// 每列条数不设上限:列内不足八条时阵列会循环重复,超过八条就向外扩充。
|
|
// 唯一的下限是每列至少一条,否则那一列无可显示。
|
|
if (records.length < 5) errors.push("records:至少需要每个分类一条档案");
|
|
// 编号宽度随总量自适应(≤999 时仍为三位,与原版一致)。
|
|
const idWidth = Math.max(3, String(records.length).length);
|
|
const ids = new Set();
|
|
records.forEach((record, index) => {
|
|
const label = `records[${index}]`;
|
|
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
errors.push(`${label}:必须是档案对象`);
|
|
return;
|
|
}
|
|
for (const key of requiredFields) {
|
|
if (!isText(record[key])) errors.push(`${label}.${key}:必须是非空文本`);
|
|
}
|
|
const expectedId = `X-${String(index + 1).padStart(idWidth, "0")}`;
|
|
if (record.id !== expectedId)
|
|
errors.push(`${label}.id:应为 ${expectedId},编号须按顺序保持稳定`);
|
|
if (ids.has(record.id)) errors.push(`${label}.id:重复编号 ${record.id}`);
|
|
ids.add(record.id);
|
|
if (!categories.includes(record.category))
|
|
errors.push(`${label}.category:未知分类 ${record.category}`);
|
|
if (
|
|
!Array.isArray(record.findings) ||
|
|
record.findings.length === 0 ||
|
|
!record.findings.every(isText)
|
|
) {
|
|
errors.push(`${label}.findings:必须包含至少一条非空研究记录`);
|
|
}
|
|
try {
|
|
const url = new URL(record.source);
|
|
// 本地归档条目用 file: 指向真实目录;保留 http(s) 以兼容公开设定类条目。
|
|
if (!["https:", "http:", "file:"].includes(url.protocol)) throw new Error();
|
|
} catch {
|
|
errors.push(`${label}.source:必须是有效的 HTTP、HTTPS 或 file 链接`);
|
|
}
|
|
});
|
|
for (const name of columns) {
|
|
const count = records.filter((record) => record?.category === name).length;
|
|
if (count < 1) errors.push(`分类“${name}”:至少要有一条档案`);
|
|
}
|
|
if (errors.length)
|
|
throw new Error(`档案数据校验失败:\n- ${errors.join("\n- ")}`);
|
|
return content;
|
|
}
|
|
|
|
export async function loadContent() {
|
|
return validateContent(
|
|
JSON.parse(
|
|
await fs.readFile(
|
|
new URL("../content/archives.json", import.meta.url),
|
|
"utf8",
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
export function archiveText(r) {
|
|
return `\uFEFFRHINE LAB · LOCAL ARCHIVE INDEX\nFILE ${r.id} / ${r.title}\n${r.en}\n\n归档位置:${r.department}\n编目范围:${r.date}\n来源:${r.lead}\n访问范围:${r.clearance}\n\n${r.abstract}\n\n研究记录\n${r.findings.map((f, i) => `${i + 1}. ${f}`).join("\n")}\n\n归档参考:${r.source}\n本文件是本地归档的编目摘要,由 scripts/archive-source.mjs 依据实际目录生成。\n`;
|
|
}
|