import { z } from "zod"; import type { ReportLanguage } from "./report-language"; const PLANET_ZH: Record = { Sun: "太阳", Moon: "月亮", Mars: "火星", Mercury: "水星", Jupiter: "木星", Venus: "金星", Saturn: "土星", Rahu: "北交点", Ketu: "南交点", }; const LEVEL_LABEL = { md: { zh: "主运", en: "Major" }, ad: { zh: "子运", en: "Sub" }, pd: { zh: "次子运", en: "Sub-sub" }, } as const; const DOMAIN_LABEL = { career: { zh: "事业", en: "Career" }, wealth: { zh: "财富", en: "Wealth" }, relationship: { zh: "婚恋", en: "Relationship" }, } as const; const day = z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable(); const lord = z.string().min(1).max(40).nullable(); export const reportFocusBlockSchema = z.strictObject({ version: z.literal(1), periods: z.tuple([ z.strictObject({ level: z.literal("md"), lord, start: day, end: day }), z.strictObject({ level: z.literal("ad"), lord, start: day, end: day }), z.strictObject({ level: z.literal("pd"), lord, start: day, end: day }), ]), domains: z.tuple([ z.strictObject({ key: z.literal("career"), house: z.literal(10), lord, placed: z.number().int().min(1).max(12).nullable(), dignity: z.string().min(1).max(80), role: z.string().min(1).max(80), }), z.strictObject({ key: z.literal("wealth"), house: z.literal(2), lord, placed: z.number().int().min(1).max(12).nullable(), dignity: z.string().min(1).max(80), role: z.string().min(1).max(80), }), z.strictObject({ key: z.literal("relationship"), house: z.literal(7), lord, placed: z.number().int().min(1).max(12).nullable(), dignity: z.string().min(1).max(80), role: z.string().min(1).max(80), }), ]), caveat: z.string().min(1).max(240), }); export type ReportFocusBlock = z.infer; export function parseReportFocusBlock(source: string): ReportFocusBlock | null { try { return reportFocusBlockSchema.parse(JSON.parse(source)); } catch { return null; } } export function planetLabel(name: string | null, language: ReportLanguage): string { if (!name) return language === "en" ? "not listed" : "未列出"; if (language === "zh") return PLANET_ZH[name] ?? name; return name; } function monthLabel(iso: string | null): string | null { if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return null; return `${iso.slice(0, 4)}.${iso.slice(5, 7)}`; } export function periodRange(start: string | null, end: string | null): string | null { const from = monthLabel(start); const to = monthLabel(end); if (from && to) return `${from} – ${to}`; return from ?? to; } export function focusBlockText(block: ReportFocusBlock, language: ReportLanguage = "zh"): string { const kicker = language === "en" ? "Current period" : "现在走的大运"; const periods = block.periods.map((period) => { const label = LEVEL_LABEL[period.level][language]; const range = periodRange(period.start, period.end); const name = planetLabel(period.lord, language); return range ? `${label} ${name} ${range}` : `${label} ${name}`; }); const domains = block.domains.map((domain) => { const label = DOMAIN_LABEL[domain.key][language]; const name = planetLabel(domain.lord, language); const placed = domain.placed === null ? (language === "en" ? "house not listed" : "落宫未列出") : (language === "en" ? `house ${domain.placed}` : `第${domain.placed}宫`); const where = language === "en" ? `${label}: House ${domain.house} lord · ${name} · ${placed}` : domain.placed === null ? `${label}:第${domain.house}宫主 · ${name} · ${placed}` : `${label}:第${domain.house}宫主 · ${name} · 落第${domain.placed}宫`; return `${where}(${domain.dignity};${domain.role})`; }); return [kicker, ...periods, ...domains, block.caveat].join("\n"); } const FOCUS_FENCE_RE = /```jyotish-focus\r?\n([\s\S]*?)```/g; /** Turn the fence into readable lines. A bad fence becomes the same invalid sentence as the page. */ export function renderReportFocusFences(markdown: string, language: ReportLanguage = "zh"): string { return markdown.replace(FOCUS_FENCE_RE, (_full, source: string) => { const block = parseReportFocusBlock(String(source).trim()); if (!block) return language === "en" ? "Focus data is invalid\n" : "重点数据无效\n"; return `${focusBlockText(block, language)}\n`; }); } export function reportTitleByline(input: { title: string; subjectName?: string | null; createdAt?: string | null; language?: ReportLanguage; }): string | null { const language = input.language ?? "zh"; const name = (input.subjectName ?? "").trim(); const dateMatch = /^(\d{4}-\d{2}-\d{2})/.exec((input.createdAt ?? "").trim()); const date = dateMatch && validDay(dateMatch[1]) ? dateMatch[1] : ""; const showName = name.length > 0 && !input.title.includes(name); if (!showName && !date) return null; if (language === "en") { if (showName && date) return `${name} · Generated ${date}`; if (showName) return name; return `Generated ${date}`; } if (showName && date) return `${name} · 生成于 ${date}`; if (showName) return name; return `生成于 ${date}`; } function validDay(iso: string): boolean { const [year, month, day] = iso.split("-").map(Number); if (!year || !month || !day) return false; if (month < 1 || month > 12 || day < 1 || day > 31) return false; return true; }