import { findForbiddenContent } from "./personal-report-contract"; import { PLANET_ZH, SIGN_ORDER, SIGN_ZH, type PlanetName, type SignName } from "./chart-view-labels"; export const record = (value: unknown): Record => value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; export const at = (value: unknown, path: string): unknown => path.split(".").reduce((next, key) => record(next)[key], value); export const entries = (value: unknown) => Object.entries(record(value)).filter(([key]) => /^[A-Za-z0-9_-]+$/.test(key)); export const list = (value: unknown): unknown[] => Array.isArray(value) ? value : []; export const decimal = (value: unknown): string | null => typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : null; export const yesNo = (value: unknown): string | null => typeof value === "boolean" ? value ? "是" : "否" : null; /** Display-only allowlist: unknown/internal labels never become prose. */ export const readable = (value: unknown): string | null => typeof value === "string" && value.length > 0 && value.length <= 300 && !/[_<>\[\]{}\/\\]|https?:|\b\w+\.\w+\b/.test(value) && !findForbiddenContent(value).length ? value : null; export const date = (value: unknown): string | null => typeof value === "string" && /^\d{4}-\d{2}-\d{2}(?:T|$)/.test(value) ? value.slice(0, 10) : null; export function planet(value: unknown, chinese?: unknown): string | null { const label = readable(chinese); if (label && /[㐀-鿿]/.test(label)) return label; return typeof value === "string" ? PLANET_ZH[value as PlanetName] ?? (value === "Lagna" ? "上升" : null) : null; } export function sign(value: unknown, chinese?: unknown, index?: unknown): string | null { const label = readable(chinese); if (label && /[㐀-鿿]/.test(label)) return label; if (typeof value === "string") { const mapped = SIGN_ZH[value as SignName]; if (mapped) return mapped; if (Object.values(SIGN_ZH).some(name => value === name || value === `${name}座`)) return value; } return typeof index === "number" && Number.isInteger(index) && index >= 0 && index < 12 ? SIGN_ZH[SIGN_ORDER[index]] : null; } export const position = (value: unknown) => { const raw = record(value); return [sign(raw.sign, raw.sign_cn, raw.sign_idx), decimal(raw.degree_in_sign ?? raw.sign_degree)] as const; }; export const isBlocked = (value: unknown) => { const raw = record(value); return raw.status === "blocked" || raw.status === "missing_in_local" || raw.claimStatus === "blocked" || raw.claim_status === "blocked"; }; /** English edition: engine keys are already English names; `Lagna` reads as Ascendant. */ export function planetEn(value: unknown): string | null { if (typeof value !== "string") return null; if (value === "Lagna") return "Ascendant"; return value in PLANET_ZH ? value : null; } /** English sign name from an engine key, a Chinese sign name, or a 0-based index. */ export function signEn(value: unknown, index?: unknown): string | null { if (typeof value === "string") { if (value in SIGN_ZH) return value; const match = SIGN_ORDER.find(name => value === SIGN_ZH[name] || value === `${SIGN_ZH[name]}座`); if (match) return match; } return typeof index === "number" && Number.isInteger(index) && index >= 0 && index < 12 ? SIGN_ORDER[index] : null; }