feat: add local hexagram content for offline reading

Load a versioned Wikisource jing plus project-authored plain drafts so results can show labeled original and vernacular texts without unauthorized modern translations.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
QiuSW
2026-08-19 11:48:16 +08:00
co-authored by Cursor
parent 4235cf9011
commit 534c88993e
33 changed files with 5626 additions and 29 deletions
+85
View File
@@ -0,0 +1,85 @@
import { readFile, writeFile, mkdir } from "node:fs/promises";
import path from "node:path";
import { kingWenEntries, validateContentPackage } from "./content-contract.mjs";
import { repositoryRoot } from "./repository-files.mjs";
import { lingjiPlain } from "../content/plain/lingji-plain.mjs";
const names = [
"乾", "坤", "屯", "蒙", "需", "讼", "师", "比",
"小畜", "履", "泰", "否", "同人", "大有", "谦", "豫",
"随", "蛊", "临", "观", "噬嗑", "贲", "剥", "复",
"无妄", "大畜", "颐", "大过", "坎", "离", "咸", "恒",
"遁", "大壮", "晋", "明夷", "家人", "睽", "蹇", "解",
"损", "益", "夬", "姤", "萃", "升", "困", "井",
"革", "鼎", "震", "艮", "渐", "归妹", "丰", "旅",
"巽", "兑", "涣", "节", "中孚", "小过", "既济", "未济",
];
if (lingjiPlain.length !== 64) {
throw new Error(`lingji plain table must contain 64 hexagrams, got ${lingjiPlain.length}`);
}
const jingPath = path.join(repositoryRoot, "content", "raw", "zhouyi-wikisource-jing.json");
const jing = JSON.parse(await readFile(jingPath, "utf8"));
if (!Array.isArray(jing) || jing.length !== 64) {
throw new Error("Wikisource jing import must contain 64 hexagrams; run scripts/import-zhouyi-wikisource.mjs");
}
const jingById = new Map(jing.map((item) => [item.kingWenNumber, item]));
const entries = kingWenEntries();
const originalSource = "wikisource-zhouyi-jing-zh-Hans";
const plainSource = "lingji-plain-zh-Hans-2026.1";
const contentPackage = {
schemaVersion: 1,
contentVersion: "zh-Hans-2026.1",
specialUsageTexts: { qian: true, kun: true },
sources: [
{
id: originalSource,
title: "维基文库《周易》易经",
edition: "仅卦辞、爻辞与乾用九、坤用六,不含十翼;由 zh.wikisource.org 导入并经 Wikimedia zh-hans 转换",
license: "公版(Wikimedia PD-old)",
url: "https://zh.wikisource.org/wiki/周易",
},
{
id: plainSource,
title: "灵机本地白话",
edition: "2026.1 项目自撰草稿,待内容负责人审校后视为可再分发",
license: "项目自撰;审校完成前仅用于本机构建,不以现代译注冒充公版",
url: "https://creativecommons.org/licenses/by/4.0/deed.zh-Hans",
},
],
hexagrams: entries.map((entry) => {
const original = jingById.get(entry.kingWenNumber);
if (!original) {
throw new Error(`Missing jing text for King Wen ${entry.kingWenNumber}`);
}
const plain = lingjiPlain[entry.kingWenNumber - 1];
if (!Array.isArray(plain) || plain.length !== 7 || plain.some((text) => !String(text).trim())) {
throw new Error(`Plain text for King Wen ${entry.kingWenNumber} must contain 7 non-blank strings`);
}
return {
...entry,
name: names[entry.kingWenNumber - 1],
symbol: String.fromCodePoint(0x4DC0 + entry.kingWenNumber - 1),
judgmentOriginal: original.judgmentOriginal,
judgmentPlain: plain[0],
lineTextsBottomUp: original.lineTextsBottomUp,
linePlainBottomUp: plain.slice(1),
specialUsageText: original.specialUsageText ?? null,
sourceRefs: [originalSource, plainSource],
};
}),
};
const errors = validateContentPackage(contentPackage);
if (errors.length > 0) {
throw new Error(`Built content package failed validation:\n${errors.join("\n")}`);
}
const outputDirectory = path.join(repositoryRoot, "content", "packages");
await mkdir(outputDirectory, { recursive: true });
const outputPath = path.join(outputDirectory, "hexagram-content.json");
await writeFile(outputPath, `${JSON.stringify(contentPackage, null, 2)}\n`);
process.stdout.write(`wrote ${outputPath}\n`);