Longform report pages kept the D1/D9/Moon and varga headings but skipHtml stripped the engine's inline SVG. Emit a jyotish-chart JSON fence beside each SVG and render a diamond chart on the reader without relaxing HTML sanitization. BUG-607. Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
3.6 KiB
TypeScript
167 lines
3.6 KiB
TypeScript
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<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>;
|
|
|
|
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");
|
|
}
|