Files
spsays-training/lib/iso.ts
T
2026-04-17 22:51:58 +05:30

346 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Data access layer for Śrī Īśopaniṣad (ISO).
*
* Reads from MongoDB via Prisma.
* ISO is unique — it has BOTH verses (in the `verses` collection)
* AND prose intro segments (in the `prose_chapters` collection).
*
* Verses were seeded with:
* - book: "iso", division: null
* - chapter = mantra number (0 for Invocation, 1-18 for mantras)
* - verse = always 1
*
* Intro segments were seeded with:
* - book: "iso", volume: 0, type: "intro-segment"
* - slugs like "intro-front-pages", "intro-introduction", "intro-about-the-author"
*/
import { prisma } from "@/lib/db";
import type { JsonValue } from "@prisma/client/runtime/library";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const BOOK = "iso";
const NO_VOLUME = 0;
// ---------------------------------------------------------------------------
// Mantra title lookup
// ---------------------------------------------------------------------------
const MANTRA_TITLES: Record<number, string> = {
0: "Invocation",
1: "Mantra One",
2: "Mantra Two",
3: "Mantra Three",
4: "Mantra Four",
5: "Mantra Five",
6: "Mantra Six",
7: "Mantra Seven",
8: "Mantra Eight",
9: "Mantra Nine",
10: "Mantra Ten",
11: "Mantra Eleven",
12: "Mantra Twelve",
13: "Mantra Thirteen",
14: "Mantra Fourteen",
15: "Mantra Fifteen",
16: "Mantra Sixteen",
17: "Mantra Seventeen",
18: "Mantra Eighteen",
};
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type IsoMeta = {
book: string;
title: string;
author: string;
year: number;
description: string;
totalMantras: number;
mantras: IsoMantraEntry[];
};
export type IsoMantraEntry = {
number: number;
title: string;
slug: string;
};
export type IsoIntroSegment = {
slug: string;
title: string;
type: string;
book: string;
order: number;
paragraphs: string[];
};
export type IsoVerse = {
book: string;
book_title: string;
mantra: number;
title: string;
reference: string;
devanagari: string;
transliteration: string;
synonyms: { word: string; meanings: string[] }[];
translation: string;
purport: string[];
};
/** Union type used in sidebar / table-of-contents navigation. */
export type IsoSidebarItem = {
/** URL-safe slug: "intro-front-pages", "invocation", "1", "intro-about-the-author" */
slug: string;
/** Human-readable label for the sidebar */
label: string;
/** "intro-segment" | "verse" */
type: string;
/** Mantra number (0 for invocation), or null for non-verse items */
mantra: number | null;
};
// ---------------------------------------------------------------------------
// HTML sanitisation (mirrors lib/nod.ts)
// ---------------------------------------------------------------------------
function sanitizeHtml(html: string): string {
let clean = html.replace(/<script[\s>][\s\S]*?<\/script>/gi, "");
clean = clean.replace(/<style[\s>][\s\S]*?<\/style>/gi, "");
clean = clean.replace(/<iframe[\s>][\s\S]*?<\/iframe>/gi, "");
clean = clean.replace(
/<\/?(?:script|style|iframe|object|embed|form|input|textarea|select|button)[^>]*>/gi,
"",
);
clean = clean.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
clean = clean.replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href=""');
clean = clean.replace(/href\s*=\s*'javascript:[^']*'/gi, "href=''");
return clean;
}
/** Normalize paragraphs — handles both native arrays and stringified JSON. */
function normalizeParagraphs(paragraphs: string[] | JsonValue): string[] {
let result: string[];
if (typeof paragraphs === "string") {
try {
const parsed = JSON.parse(paragraphs);
result = Array.isArray(parsed) ? parsed : [paragraphs];
} catch {
result = [paragraphs];
}
} else if (Array.isArray(paragraphs)) {
result = paragraphs as string[];
} else {
result = [];
}
return result.map(sanitizeHtml);
}
/** Normalize purport (string[] from Prisma) via normalizeParagraphs. */
function normalizePurport(purport: string[] | JsonValue): string[] {
return normalizeParagraphs(purport);
}
// ---------------------------------------------------------------------------
// Book meta
// ---------------------------------------------------------------------------
/**
* Book-level metadata (title, author, mantras list, etc.)
*/
export async function getIsoMeta(): Promise<IsoMeta | null> {
const meta = await prisma.bookMeta.findUnique({ where: { book: BOOK } });
if (!meta) return null;
return meta.data as unknown as IsoMeta;
}
// ---------------------------------------------------------------------------
// Intro segments (from prose_chapters collection)
// ---------------------------------------------------------------------------
/**
* All intro segments, ordered by their `order` field.
*/
export async function getIsoIntroSegments(): Promise<IsoIntroSegment[]> {
const results = await prisma.proseChapter.findMany({
where: { book: BOOK, volume: NO_VOLUME, type: "intro-segment" },
orderBy: { order: "asc" },
});
return results.map((doc) => ({
slug: doc.slug,
title: doc.title,
type: doc.type,
book: doc.book,
order: doc.order,
paragraphs: normalizeParagraphs(doc.paragraphs),
}));
}
/**
* A single intro segment by slug.
* Accepts both "intro-front-pages" (URL slug) and "front-pages" (raw slug).
*/
export async function getIsoIntroSegment(
slug: string,
): Promise<IsoIntroSegment | null> {
// DB stores with "intro-" prefix; accept either form
const dbSlug = slug.startsWith("intro-") ? slug : `intro-${slug}`;
const doc = await prisma.proseChapter.findUnique({
where: {
book_volume_slug: { book: BOOK, volume: NO_VOLUME, slug: dbSlug },
},
});
if (!doc) return null;
return {
slug: doc.slug,
title: doc.title,
type: doc.type,
book: doc.book,
order: doc.order,
paragraphs: normalizeParagraphs(doc.paragraphs),
};
}
// ---------------------------------------------------------------------------
// Verses (from verses collection)
// ---------------------------------------------------------------------------
/**
* A single verse/mantra by slug.
*
* - "invocation" → chapter=0, verse=1
* - "1"-"18" → chapter=1–18, verse=1
*/
export async function getIsoVerse(slug: string): Promise<IsoVerse | null> {
let mantraNum: number;
if (slug === "invocation") {
mantraNum = 0;
} else {
const num = Number(slug);
if (!Number.isInteger(num) || num < 1 || num > 18) return null;
mantraNum = num;
}
const doc = await prisma.verse.findFirst({
where: { book: BOOK, division: null, chapter: mantraNum, verse: 1 },
});
if (!doc) return null;
return {
book: doc.book,
book_title: doc.bookTitle,
mantra: doc.chapter, // chapter field stores the mantra number
title: MANTRA_TITLES[doc.chapter] ?? `Mantra ${doc.chapter}`,
reference: doc.reference,
devanagari: doc.devanagari,
transliteration: doc.transliteration,
synonyms: doc.synonyms as { word: string; meanings: string[] }[],
translation: sanitizeHtml(doc.translation),
purport: normalizePurport(doc.purport),
};
}
// ---------------------------------------------------------------------------
// Unified content accessor
// ---------------------------------------------------------------------------
/**
* Get content by URL slug.
*
* "intro-front-pages" → intro segment
* "intro-introduction" → intro segment
* "intro-about-the-author" → intro segment
* "invocation" → invocation verse
* "1" … "18" → mantra verse
*/
export async function getIsoContent(
slug: string,
): Promise<IsoVerse | IsoIntroSegment | null> {
if (slug.startsWith("intro-")) {
return getIsoIntroSegment(slug);
}
if (slug === "invocation") {
return getIsoVerse("invocation");
}
const num = Number(slug);
if (!Number.isNaN(num) && Number.isInteger(num) && num >= 1 && num <= 18) {
return getIsoVerse(String(num));
}
return null;
}
// ---------------------------------------------------------------------------
// Sidebar navigation
// ---------------------------------------------------------------------------
/**
* Build the full sidebar navigation list.
*
* Order: front-pages → introduction → invocation → mantras 1-18 → about-the-author
*/
export async function getIsoSidebarItems(): Promise<IsoSidebarItem[]> {
const meta = await getIsoMeta();
if (!meta) return [];
const items: IsoSidebarItem[] = [];
// 1. Front pages intro segment
items.push({
slug: "intro-front-pages",
label: "Back Cover & Front Pages",
type: "intro-segment",
mantra: null,
});
// 2. Introduction (Teachings of the Vedas)
items.push({
slug: "intro-introduction",
label: "Introduction (Teachings of the Vedas)",
type: "intro-segment",
mantra: null,
});
// 3. Invocation + Mantras 1-18 (from meta.mantras)
for (const m of meta.mantras) {
if (m.number === 0) {
items.push({
slug: "invocation",
label: "Invocation",
type: "verse",
mantra: 0,
});
} else {
items.push({
slug: String(m.number),
label: `${m.number}. ${m.title}`,
type: "verse",
mantra: m.number,
});
}
}
// 4. About the Author intro segment (at the end)
items.push({
slug: "intro-about-the-author",
label: "About the Author",
type: "intro-segment",
mantra: null,
});
return items;
}