北印星盘(星盘页 / 校正右栏 / 我的报告共用 VedicChartSvg)宫内不再画 「水 19°」「罗逆 29°」这类显示文本——一宫三颗星就叠三行、字号压到 8。 改成九个彩色符号横排,每行最多 3 个;逆行是符号下方一道同色横线, 第 1 宫加 As 小标,盘下方两行图例。度数、星宿、顺逆下沉到星盘页的 行星表,列从四列扩到八列,首行是上升,度数精确到分。 - 符号字体:vendored 的 9 字形 Noto Sans Symbols 子集(OFL,1.5 KB), 经 next/font/local 挂成 --font-planet-glyphs;♀ ♂ 带 U+FE0E 防 iOS 画成表情。构建镜像不能联网,所以必须自带,与 Inter 同一条理由。 - 颜色:--color-planet-* 九个 token,:root 与两个深色块各一份。只用于 认星不表吉凶,罗计共用中性灰。挂 is-* 修饰类而非行内 style——报告 文档的标记合同禁止渲染结果出现 style=,星盘不做例外。 - 数据来源:toNorthIndianChart() 加结构化 occupantGlyphs(星盘页与 markdown fence 走这条);校正与报告两条路线的模型只有引擎 / golden 显示文本,按首字解析,解析由测试锁住。 - 星宿沿用引擎返回名:仓库无中文宿名表,印度 27 宿与中国二十八宿不是 同一套,不自造对照。 tsc 0 错,lint 0 error,npm test 3471→3484 条、36 红与基线逐条相同, / 仍 Static,首屏 JS gzip +0.29%。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
206 lines
5.0 KiB
TypeScript
206 lines
5.0 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { PLANET_GLYPH_KEYS, type PlanetGlyphKey } from "./planet-glyphs";
|
|
|
|
const SIGNS = [
|
|
"Aries",
|
|
"Taurus",
|
|
"Gemini",
|
|
"Cancer",
|
|
"Leo",
|
|
"Virgo",
|
|
"Libra",
|
|
"Scorpio",
|
|
"Sagittarius",
|
|
"Capricorn",
|
|
"Aquarius",
|
|
"Pisces",
|
|
] as const;
|
|
|
|
const PLANETS = [
|
|
"Sun",
|
|
"Moon",
|
|
"Mars",
|
|
"Mercury",
|
|
"Jupiter",
|
|
"Venus",
|
|
"Saturn",
|
|
"Rahu",
|
|
"Ketu",
|
|
] as const;
|
|
|
|
const PLANET_LABEL: Record<(typeof PLANETS)[number], string> = {
|
|
Sun: "日",
|
|
Moon: "月",
|
|
Mars: "火",
|
|
Mercury: "水",
|
|
Jupiter: "木",
|
|
Venus: "金",
|
|
Saturn: "土",
|
|
Rahu: "罗",
|
|
Ketu: "计",
|
|
};
|
|
|
|
const SIGN_NUMBER: Record<string, number> = {
|
|
Aries: 1,
|
|
Taurus: 2,
|
|
Gemini: 3,
|
|
Cancer: 4,
|
|
Leo: 5,
|
|
Virgo: 6,
|
|
Libra: 7,
|
|
Scorpio: 8,
|
|
Sagittarius: 9,
|
|
Capricorn: 10,
|
|
Aquarius: 11,
|
|
Pisces: 12,
|
|
白羊: 1,
|
|
金牛: 2,
|
|
双子: 3,
|
|
巨蟹: 4,
|
|
狮子: 5,
|
|
处女: 6,
|
|
天秤: 7,
|
|
天蝎: 8,
|
|
射手: 9,
|
|
摩羯: 10,
|
|
水瓶: 11,
|
|
双鱼: 12,
|
|
白羊座: 1,
|
|
金牛座: 2,
|
|
双子座: 3,
|
|
巨蟹座: 4,
|
|
狮子座: 5,
|
|
处女座: 6,
|
|
天秤座: 7,
|
|
天蝎座: 8,
|
|
射手座: 9,
|
|
摩羯座: 10,
|
|
水瓶座: 11,
|
|
双鱼座: 12,
|
|
};
|
|
|
|
const FENCE_RE = /(?:\n[ \t]*)?```jyotish-chart\n[\s\S]*?```(?:\n[ \t]*)?/g;
|
|
|
|
export const reportChartBlockSchema = z.strictObject({
|
|
version: z.literal(1),
|
|
id: z.string().regex(/^(D\d{1,3}|MOON)$/),
|
|
title: z.string().max(120),
|
|
layout: z.literal("north"),
|
|
ascendant: z.strictObject({
|
|
sign: z.enum(SIGNS),
|
|
degree: z.number().gte(0).lt(30),
|
|
}),
|
|
planets: z.array(z.strictObject({
|
|
name: z.enum(PLANETS),
|
|
sign: z.enum(SIGNS),
|
|
degree: z.number().gte(0).lt(30),
|
|
retrograde: z.boolean(),
|
|
})).max(9),
|
|
});
|
|
|
|
export type ReportChartBlock = z.infer<typeof reportChartBlockSchema>;
|
|
|
|
/**
|
|
* `occupants` stays the display string — it is what `aria-label`, the house
|
|
* `<details>` table and screen readers read out. `occupantGlyphs` is the
|
|
* structured twin the chart draws from, so the renderer never has to read a
|
|
* body or its direction back out of formatted text. Routes whose house table
|
|
* arrives as engine/agent text (the rectification board, the personal report
|
|
* document) omit it and are parsed instead.
|
|
*/
|
|
export type NorthIndianChartModel = {
|
|
houses: {
|
|
houseNumber: number;
|
|
sign: string;
|
|
occupants: string[];
|
|
occupantGlyphs?: { key: PlanetGlyphKey; retrograde: boolean; degree: number }[];
|
|
}[];
|
|
planets?: { name: string; retrograde: boolean }[];
|
|
};
|
|
|
|
export function parseReportChartBlock(source: string): ReportChartBlock | null {
|
|
try {
|
|
return reportChartBlockSchema.parse(JSON.parse(source));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function integerDegree(degree: number): number {
|
|
const value = Math.floor(degree);
|
|
if (value >= 30) return 29;
|
|
if (value < 0) return 0;
|
|
return value;
|
|
}
|
|
|
|
export function planetDisplayLabel(
|
|
name: (typeof PLANETS)[number],
|
|
degree: number,
|
|
retrograde: boolean,
|
|
): string {
|
|
const body = PLANET_LABEL[name];
|
|
const suffix = retrograde ? "逆" : "";
|
|
return `${body}${suffix} ${integerDegree(degree)}°`;
|
|
}
|
|
|
|
export function signOrdinal(sign: string): number | null {
|
|
return SIGN_NUMBER[sign] ?? null;
|
|
}
|
|
|
|
export function chartAriaLabel(id: ReportChartBlock["id"]): string {
|
|
return id === "MOON" ? "月亮参考盘" : `${id} 北印度盘`;
|
|
}
|
|
|
|
const PLANET_GLYPH_KEY: Record<(typeof PLANETS)[number], PlanetGlyphKey> = {
|
|
Sun: "sun",
|
|
Moon: "moon",
|
|
Mars: "mars",
|
|
Mercury: "mercury",
|
|
Jupiter: "jupiter",
|
|
Venus: "venus",
|
|
Saturn: "saturn",
|
|
Rahu: "rahu",
|
|
Ketu: "ketu",
|
|
};
|
|
|
|
export function isPlanetGlyphKey(value: string): value is PlanetGlyphKey {
|
|
return (PLANET_GLYPH_KEYS as readonly string[]).includes(value);
|
|
}
|
|
|
|
export function toNorthIndianChart(block: ReportChartBlock): NorthIndianChartModel {
|
|
const ascIndex = SIGNS.indexOf(block.ascendant.sign);
|
|
const occupantsByHouse: string[][] = Array.from({ length: 12 }, () => []);
|
|
const glyphsByHouse: NonNullable<NorthIndianChartModel["houses"][number]["occupantGlyphs"]>[] =
|
|
Array.from({ length: 12 }, () => []);
|
|
for (const planet of block.planets) {
|
|
const signIndex = SIGNS.indexOf(planet.sign);
|
|
const houseNumber = ((signIndex - ascIndex + 12) % 12) + 1;
|
|
occupantsByHouse[houseNumber - 1].push(
|
|
planetDisplayLabel(planet.name, planet.degree, planet.retrograde),
|
|
);
|
|
glyphsByHouse[houseNumber - 1].push({
|
|
key: PLANET_GLYPH_KEY[planet.name],
|
|
retrograde: planet.retrograde,
|
|
degree: planet.degree,
|
|
});
|
|
}
|
|
return {
|
|
houses: Array.from({ length: 12 }, (_, index) => ({
|
|
houseNumber: index + 1,
|
|
sign: SIGNS[(ascIndex + index + 12) % 12],
|
|
occupants: occupantsByHouse[index],
|
|
occupantGlyphs: glyphsByHouse[index],
|
|
})),
|
|
planets: block.planets.map((planet) => ({
|
|
name: PLANET_LABEL[planet.name],
|
|
retrograde: planet.retrograde,
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function stripReportChartBlocks(markdown: string): string {
|
|
const stripped = markdown.replace(FENCE_RE, "\n\n");
|
|
return stripped.replace(/\n{3,}/g, "\n\n");
|
|
}
|