Files
spsays-training/lib/bs.ts
T

358 lines
9.2 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ī Brahma-saṁhitā (BS).
*
* Reads from MongoDB via Prisma.
* BS has BOTH verses (in the `verses` collection)
* AND prose intro segments (in the `prose_chapters` collection).
*
* Verses were seeded with:
* - book: "bs", division: null
* - chapter = text number (1-62)
* - verse = always 1
*
* Intro segments were seeded with:
* - book: "bs", volume: 0, type: "intro-segment"
* - slugs like "intro-introduction", "intro-foreword"
*/
import { prisma } from "@/lib/db";
import type { JsonValue } from "@prisma/client/runtime/library";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const BOOK = "bs";
const NO_VOLUME = 0;
// ---------------------------------------------------------------------------
// Text title lookup
// ---------------------------------------------------------------------------
const TEXT_TITLES: Record<number, string> = {
1: "Text 1",
2: "Text 2",
3: "Text 3",
4: "Text 4",
5: "Text 5",
6: "Text 6",
7: "Text 7",
8: "Text 8",
9: "Text 9",
10: "Text 10",
11: "Text 11",
12: "Text 12",
13: "Text 13",
14: "Text 14",
15: "Text 15",
16: "Text 16",
17: "Text 17",
18: "Text 18",
19: "Text 19",
20: "Text 20",
21: "Text 21",
22: "Text 22",
23: "Text 23",
24: "Text 24",
25: "Text 25",
26: "Text 26",
27: "Text 27",
28: "Text 28",
29: "Text 29",
30: "Text 30",
31: "Text 31",
32: "Text 32",
33: "Text 33",
34: "Text 34",
35: "Text 35",
36: "Text 36",
37: "Text 37",
38: "Text 38",
39: "Text 39",
40: "Text 40",
41: "Text 41",
42: "Text 42",
43: "Text 43",
44: "Text 44",
45: "Text 45",
46: "Text 46",
47: "Text 47",
48: "Text 48",
49: "Text 49",
50: "Text 50",
51: "Text 51",
52: "Text 52",
53: "Text 53",
54: "Text 54",
55: "Text 55",
56: "Text 56",
57: "Text 57",
58: "Text 58",
59: "Text 59",
60: "Text 60",
61: "Text 61",
62: "Text 62",
};
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type BsMeta = {
book: string;
title: string;
author: string;
year: number;
description: string;
totalTexts: number;
texts: BsTextEntry[];
};
export type BsTextEntry = {
number: number;
title: string;
slug: string;
};
export type BsIntroSegment = {
slug: string;
title: string;
type: string;
book: string;
order: number;
paragraphs: string[];
};
export type BsVerse = {
book: string;
book_title: string;
text: number; // 1-62
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 BsSidebarItem = {
/** URL-safe slug: "intro-introduction", "intro-foreword", "1" … "62" */
slug: string;
/** Human-readable label for the sidebar */
label: string;
/** "intro-segment" | "verse" */
type: string;
/** Text number for verses, or null for intro segments */
text: number | null;
};
// ---------------------------------------------------------------------------
// HTML sanitisation (mirrors lib/iso.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, texts list, etc.)
*/
export async function getBsMeta(): Promise<BsMeta | null> {
const meta = await prisma.bookMeta.findUnique({ where: { book: BOOK } });
if (!meta) return null;
return meta.data as unknown as BsMeta;
}
// ---------------------------------------------------------------------------
// Intro segments (from prose_chapters collection)
// ---------------------------------------------------------------------------
/**
* All intro segments, ordered by their `order` field.
*/
export async function getBsIntroSegments(): Promise<BsIntroSegment[]> {
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-introduction" (URL slug) and "introduction" (raw slug).
*/
export async function getBsIntroSegment(
slug: string,
): Promise<BsIntroSegment | 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/text by slug.
*
* - "1"-"62" → chapter=1–62, verse=1
*
* Verse stored as chapter=text_number, verse=1 in the verses collection.
*/
export async function getBsVerse(slug: string): Promise<BsVerse | null> {
const num = Number(slug);
if (!Number.isInteger(num) || num < 1 || num > 62) return null;
const textNum = num;
const doc = await prisma.verse.findFirst({
where: { book: BOOK, division: null, chapter: textNum, verse: 1 },
});
if (!doc) return null;
return {
book: doc.book,
book_title: doc.bookTitle,
text: doc.chapter, // chapter field stores the text number
title: TEXT_TITLES[doc.chapter] ?? `Text ${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-introduction" → intro segment
* "intro-foreword" → intro segment
* "1" … "62" → verse (text)
*/
export async function getBsContent(
slug: string,
): Promise<BsVerse | BsIntroSegment | null> {
if (slug.startsWith("intro-")) {
return getBsIntroSegment(slug);
}
const num = Number(slug);
if (!Number.isNaN(num) && Number.isInteger(num) && num >= 1 && num <= 62) {
return getBsVerse(String(num));
}
return null;
}
// ---------------------------------------------------------------------------
// Sidebar navigation
// ---------------------------------------------------------------------------
/**
* Build the full sidebar navigation list.
*
* Order: Introduction → Foreword → Texts 1-62
*/
export async function getBsSidebarItems(): Promise<BsSidebarItem[]> {
const items: BsSidebarItem[] = [];
// 1. Introduction intro segment
items.push({
slug: "intro-introduction",
label: "Introduction",
type: "intro-segment",
text: null,
});
// 2. Foreword intro segment
items.push({
slug: "intro-foreword",
label: "Foreword",
type: "intro-segment",
text: null,
});
// 3. Texts 1-62 (verses)
for (let i = 1; i <= 62; i++) {
items.push({
slug: String(i),
label: `${i}. ${TEXT_TITLES[i]}`,
type: "verse",
text: i,
});
}
return items;
}