import { z } from "zod"; 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 = { 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; export type NorthIndianChartModel = { houses: { houseNumber: number; sign: string; occupants: string[] }[]; 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} 北印度盘`; } export function toNorthIndianChart(block: ReportChartBlock): NorthIndianChartModel { const ascIndex = SIGNS.indexOf(block.ascendant.sign); const occupantsByHouse: string[][] = 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), ); } return { houses: Array.from({ length: 12 }, (_, index) => ({ houseNumber: index + 1, sign: SIGNS[(ascIndex + index + 12) % 12], occupants: occupantsByHouse[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"); }