187 lines
5.5 KiB
TypeScript
187 lines
5.5 KiB
TypeScript
/**
|
||
* Book + chapter derivation for transcriptions.
|
||
*
|
||
* Transcriptions carry a free-text scripture `reference` (e.g. "SB 1.2.3",
|
||
* "BG Introduction", "CC Madhya-līlā 20.101") rather than structured
|
||
* book/chapter columns. This module is the single source of truth for turning
|
||
* those strings into a book identity and a chapter grouping, reused by the
|
||
* books grid, the listing page, and the book browser.
|
||
*
|
||
* The book registry mirrors `TranscriptionBooks` in the Go BFF
|
||
* (internal/repository/transcription_repo.go).
|
||
*/
|
||
|
||
export type BookCode = "SB" | "BG" | "CC" | "BS" | "NOD" | "ISO";
|
||
|
||
export const BOOKS: Record<BookCode, { title: string; order: number }> = {
|
||
SB: { title: "Śrīmad-Bhāgavatam", order: 1 },
|
||
BG: { title: "Bhagavad-gītā", order: 2 },
|
||
CC: { title: "Caitanya-caritāmṛta", order: 3 },
|
||
BS: { title: "Brahma-saṁhitā", order: 4 },
|
||
NOD: { title: "The Nectar of Devotion", order: 5 },
|
||
ISO: { title: "Śrī Īśopaniṣad", order: 6 },
|
||
};
|
||
|
||
/** Display title for a (possibly unknown) book code; "" if unknown. */
|
||
export function bookTitle(code: string): string {
|
||
const b = BOOKS[code.toUpperCase() as BookCode];
|
||
return b ? b.title : "";
|
||
}
|
||
|
||
/**
|
||
* Extract the (starting) verse number from a reference, for ordering lectures
|
||
* within a chapter. The verse is the last dotted numeric component, and for a
|
||
* range we take the start: "BG 2.13" → 13, "SB 1.2.3" → 3, "SB 3.27.11–16" →
|
||
* 11, "CC Madhya-līlā 20.101" → 101, "ISO Mantra 4" → 4. References with no
|
||
* number (e.g. bare "NOD") sort last (stable → keep their incoming order).
|
||
*/
|
||
export function parseVerse(reference: string | null): number {
|
||
const m = (reference ?? "").trim().match(/(\d+(?:\.\d+)*)/);
|
||
if (!m) return Number.MAX_SAFE_INTEGER;
|
||
const parts = m[1].split(".");
|
||
return Number(parts[parts.length - 1]);
|
||
}
|
||
|
||
/** A section is a canto (SB) or līlā (CC); null for books without that level. */
|
||
export type ChapterSection = {
|
||
key: string;
|
||
label: string;
|
||
order: number;
|
||
};
|
||
|
||
/** A chapter grouping for the accordion list. */
|
||
export type ChapterGroup = {
|
||
key: string;
|
||
label: string;
|
||
sort: [number, number];
|
||
/** Canto/līlā this chapter belongs to (SB, CC); null for flat books. */
|
||
section: ChapterSection | null;
|
||
};
|
||
|
||
// Caitanya-caritāmṛta līlā ordering (Ādi → Madhya → Antya).
|
||
const LILA_ORDER: Record<string, { label: string; order: number }> = {
|
||
ADI: { label: "Ādi-līlā", order: 1 },
|
||
MADHYA: { label: "Madhya-līlā", order: 2 },
|
||
ANTYA: { label: "Antya-līlā", order: 3 },
|
||
};
|
||
|
||
const OTHER: ChapterGroup = {
|
||
key: "other",
|
||
label: "Other",
|
||
sort: [9999, 0],
|
||
section: null,
|
||
};
|
||
|
||
/**
|
||
* Parse a transcription reference into its chapter group, given the book code.
|
||
* Tolerates the corpus's noise (en-dash vs hyphen, "Madhya" vs "Madhya-līlā",
|
||
* a stray "SB 7", etc.). Unparseable / missing references fall into "Other".
|
||
*/
|
||
export function parseChapter(code: string, reference: string | null): ChapterGroup {
|
||
const ref = (reference ?? "").trim();
|
||
if (!ref) return OTHER;
|
||
|
||
switch (code.toUpperCase() as BookCode) {
|
||
case "BG": {
|
||
if (/^BG\s+Introduction/i.test(ref)) {
|
||
return {
|
||
key: "bg-intro",
|
||
label: "Introduction",
|
||
sort: [0, 0],
|
||
section: null,
|
||
};
|
||
}
|
||
const m = ref.match(/^BG\s+(\d+)/);
|
||
if (m) {
|
||
const n = Number(m[1]);
|
||
return {
|
||
key: `bg-${n}`,
|
||
label: `Chapter ${n}`,
|
||
sort: [n, 0],
|
||
section: null,
|
||
};
|
||
}
|
||
return OTHER;
|
||
}
|
||
case "SB": {
|
||
const m = ref.match(/^SB\s+(\d+)\.(\d+)/);
|
||
if (m) {
|
||
const c = Number(m[1]);
|
||
const ch = Number(m[2]);
|
||
return {
|
||
key: `sb-${c}-${ch}`,
|
||
label: `Chapter ${ch}`,
|
||
sort: [c, ch],
|
||
section: { key: `sb-${c}`, label: `Canto ${c}`, order: c },
|
||
};
|
||
}
|
||
const cantoOnly = ref.match(/^SB\s+(\d+)\b/);
|
||
if (cantoOnly) {
|
||
const c = Number(cantoOnly[1]);
|
||
return {
|
||
key: `sb-${c}-0`,
|
||
label: "Other",
|
||
sort: [c, 0],
|
||
section: { key: `sb-${c}`, label: `Canto ${c}`, order: c },
|
||
};
|
||
}
|
||
return OTHER;
|
||
}
|
||
case "CC": {
|
||
const m = ref.match(/^CC\s+(Ādi|Adi|Madhya|Antya)(?:-līlā)?\s+(\d+)/i);
|
||
if (m) {
|
||
const lila = LILA_ORDER[m[1].toUpperCase().replace("Ā", "A")];
|
||
const ch = Number(m[2]);
|
||
if (lila) {
|
||
return {
|
||
key: `cc-${lila.order}-${ch}`,
|
||
label: `Chapter ${ch}`,
|
||
sort: [lila.order, ch],
|
||
section: {
|
||
key: `cc-${lila.order}`,
|
||
label: lila.label,
|
||
order: lila.order,
|
||
},
|
||
};
|
||
}
|
||
}
|
||
return OTHER;
|
||
}
|
||
case "BS": {
|
||
const m = ref.match(/^BS\s+(\d+)/);
|
||
if (m) {
|
||
const n = Number(m[1]);
|
||
return {
|
||
key: `bs-${n}`,
|
||
label: `Chapter ${n}`,
|
||
sort: [n, 0],
|
||
section: null,
|
||
};
|
||
}
|
||
return OTHER;
|
||
}
|
||
case "ISO": {
|
||
const m = ref.match(/^ISO\s+Mantra\s+(\d+)/i);
|
||
if (m) {
|
||
const n = Number(m[1]);
|
||
return {
|
||
key: `iso-${n}`,
|
||
label: `Mantra ${n}`,
|
||
sort: [n, 0],
|
||
section: null,
|
||
};
|
||
}
|
||
return { key: "iso", label: "Śrī Īśopaniṣad", sort: [0, 0], section: null };
|
||
}
|
||
case "NOD":
|
||
return {
|
||
key: "nod",
|
||
label: "The Nectar of Devotion",
|
||
sort: [0, 0],
|
||
section: null,
|
||
};
|
||
default:
|
||
return OTHER;
|
||
}
|
||
}
|