feat: kb both volumes
This commit is contained in:
@@ -0,0 +1,205 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { getProseChapter, getProseChapters } from "@/lib/api";
|
||||||
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export default async function KbChapterPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ volume: string; slug: string }>;
|
||||||
|
}) {
|
||||||
|
const { volume, slug } = await params;
|
||||||
|
const vol = parseInt(volume);
|
||||||
|
if (isNaN(vol) || vol < 1 || vol > 2) notFound();
|
||||||
|
|
||||||
|
const chapter = await getProseChapter("kb", vol, slug);
|
||||||
|
if (!chapter) notFound();
|
||||||
|
|
||||||
|
// Fetch all chapters/segments in this volume for sidebar + prev/next
|
||||||
|
const allChapters = await getProseChapters("kb", vol);
|
||||||
|
const currentIdx = allChapters.findIndex((ch) => ch.slug === slug);
|
||||||
|
|
||||||
|
const prev = currentIdx > 0 ? allChapters[currentIdx - 1] : null;
|
||||||
|
const next =
|
||||||
|
currentIdx < allChapters.length - 1 ? allChapters[currentIdx + 1] : null;
|
||||||
|
|
||||||
|
const chapterLabel =
|
||||||
|
chapter.type === "chapter" && chapter.chapter != null
|
||||||
|
? `Chapter ${chapter.chapter}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<PageHeader sticky wide>
|
||||||
|
<Link
|
||||||
|
href={`/books/kb/${vol}`}
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
← Volume {vol}
|
||||||
|
</Link>
|
||||||
|
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||||
|
{chapterLabel && (
|
||||||
|
<span className="text-primary">{chapterLabel}: </span>
|
||||||
|
)}
|
||||||
|
{chapter.title}
|
||||||
|
</h1>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div className="w-full md:max-w-6xl mx-auto flex">
|
||||||
|
{/* Left sidebar — chapter index */}
|
||||||
|
<ChapterSidebar
|
||||||
|
chapters={allChapters}
|
||||||
|
currentSlug={slug}
|
||||||
|
volume={vol}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
|
||||||
|
{/* Top navigation */}
|
||||||
|
<ChapterNav prev={prev} next={next} volume={vol} />
|
||||||
|
|
||||||
|
{/* Chapter content */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-8 px-6 sm:px-8">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{chapter.paragraphs.map((paragraph, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="text-foreground leading-relaxed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Bottom navigation */}
|
||||||
|
<ChapterNav prev={prev} next={next} volume={vol} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sidebar — chapter index
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type SidebarChapter = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
chapter: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ChapterSidebar({
|
||||||
|
chapters,
|
||||||
|
currentSlug,
|
||||||
|
volume,
|
||||||
|
}: {
|
||||||
|
chapters: SidebarChapter[];
|
||||||
|
currentSlug: string;
|
||||||
|
volume: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
|
||||||
|
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||||
|
Chapters
|
||||||
|
</h2>
|
||||||
|
<nav className="flex flex-col gap-0.5">
|
||||||
|
{chapters.map((ch) => {
|
||||||
|
const isActive = ch.slug === currentSlug;
|
||||||
|
const label =
|
||||||
|
ch.type === "chapter" && ch.chapter != null
|
||||||
|
? `${ch.chapter}. ${ch.title}`
|
||||||
|
: ch.title;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={ch.slug}
|
||||||
|
href={`/books/kb/${volume}/${ch.slug}`}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
|
||||||
|
isActive
|
||||||
|
? "bg-primary/10 text-primary font-medium"
|
||||||
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
|
)}
|
||||||
|
title={label}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Prev / Next navigation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type NavChapter = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
chapter: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ChapterNav({
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
volume,
|
||||||
|
}: {
|
||||||
|
prev: NavChapter | null;
|
||||||
|
next: NavChapter | null;
|
||||||
|
volume: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
{prev ? (
|
||||||
|
<Link
|
||||||
|
href={`/books/kb/${volume}/${prev.slug}`}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline", size: "sm" }),
|
||||||
|
"gap-1.5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="size-4" />
|
||||||
|
<span className="hidden sm:inline truncate max-w-[180px]">
|
||||||
|
{prev.type === "chapter" && prev.chapter != null
|
||||||
|
? `Ch. ${prev.chapter}`
|
||||||
|
: prev.title}
|
||||||
|
</span>
|
||||||
|
<span className="sm:hidden">Previous</span>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{next ? (
|
||||||
|
<Link
|
||||||
|
href={`/books/kb/${volume}/${next.slug}`}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline", size: "sm" }),
|
||||||
|
"gap-1.5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="hidden sm:inline truncate max-w-[180px]">
|
||||||
|
{next.type === "chapter" && next.chapter != null
|
||||||
|
? `Ch. ${next.chapter}`
|
||||||
|
: next.title}
|
||||||
|
</span>
|
||||||
|
<span className="sm:hidden">Next</span>
|
||||||
|
<ChevronRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { getBookMeta, getProseChapters } from "@/lib/api";
|
||||||
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
|
export default async function KbVolumePage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ volume: string }>;
|
||||||
|
}) {
|
||||||
|
const { volume } = await params;
|
||||||
|
const vol = parseInt(volume);
|
||||||
|
if (isNaN(vol) || vol < 1 || vol > 2) notFound();
|
||||||
|
|
||||||
|
const meta = await getBookMeta("kb");
|
||||||
|
if (!meta) notFound();
|
||||||
|
|
||||||
|
const title = meta.title as string;
|
||||||
|
|
||||||
|
const [introSegments, chapters] = await Promise.all([
|
||||||
|
getProseChapters("kb", vol, "intro-segment"),
|
||||||
|
getProseChapters("kb", vol, "chapter"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<PageHeader>
|
||||||
|
<Link
|
||||||
|
href="/books/kb"
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
← {title}
|
||||||
|
</Link>
|
||||||
|
<h1 className="mt-3 text-3xl font-bold text-foreground">
|
||||||
|
Volume {vol}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1 text-muted-foreground">{chapters.length} chapters</p>
|
||||||
|
</PageHeader>
|
||||||
|
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
|
||||||
|
{/* Front Matter */}
|
||||||
|
{introSegments.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||||
|
Front Matter
|
||||||
|
</h2>
|
||||||
|
<Card className="divide-y divide-border">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<ul className="divide-y divide-border">
|
||||||
|
{introSegments.map((seg) => (
|
||||||
|
<li key={seg.slug}>
|
||||||
|
<Link
|
||||||
|
href={`/books/kb/${vol}/${seg.slug}`}
|
||||||
|
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-foreground">
|
||||||
|
{seg.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-muted-foreground shrink-0 ml-4">
|
||||||
|
{seg.paragraphs.length} paragraphs
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Chapters */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||||
|
Chapters
|
||||||
|
</h2>
|
||||||
|
<Card className="divide-y divide-border">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<ul className="divide-y divide-border">
|
||||||
|
{chapters.map((ch) => (
|
||||||
|
<li key={ch.slug}>
|
||||||
|
<Link
|
||||||
|
href={`/books/kb/${vol}/${ch.slug}`}
|
||||||
|
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-primary font-medium">
|
||||||
|
Chapter {ch.chapter}
|
||||||
|
</span>
|
||||||
|
<p className="font-medium text-foreground">
|
||||||
|
{ch.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-muted-foreground shrink-0 ml-4">
|
||||||
|
{ch.paragraphs.length} paragraphs
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { getBookMeta } from "@/lib/api";
|
||||||
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
type VolumeInfo = {
|
||||||
|
number: number;
|
||||||
|
totalChapters: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function KbPage() {
|
||||||
|
const meta = await getBookMeta("kb");
|
||||||
|
if (!meta) notFound();
|
||||||
|
|
||||||
|
const title = meta.title as string;
|
||||||
|
const author = meta.author as string;
|
||||||
|
const description = meta.description as string | undefined;
|
||||||
|
const volumes = meta.volumes as VolumeInfo[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<PageHeader>
|
||||||
|
<Link
|
||||||
|
href="/books"
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
← Books
|
||||||
|
</Link>
|
||||||
|
<h1 className="mt-3 text-3xl font-bold text-foreground">{title}</h1>
|
||||||
|
<p className="mt-1 text-muted-foreground">{author}</p>
|
||||||
|
</PageHeader>
|
||||||
|
<main className="max-w-3xl mx-auto px-6 py-8">
|
||||||
|
{description && (
|
||||||
|
<p className="text-muted-foreground mb-6">{description}</p>
|
||||||
|
)}
|
||||||
|
<Card className="divide-y divide-border">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<ul className="divide-y divide-border">
|
||||||
|
{volumes.map((vol) => (
|
||||||
|
<li key={vol.number}>
|
||||||
|
<Link
|
||||||
|
href={`/books/kb/${vol.number}`}
|
||||||
|
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Badge variant="secondary" className="mb-1">
|
||||||
|
Volume {vol.number}
|
||||||
|
</Badge>
|
||||||
|
<p className="font-medium text-foreground">
|
||||||
|
{vol.totalChapters} chapters
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,12 @@ interface ChapterBookMeta extends BookMeta {
|
|||||||
chapters: ChapterEntry[];
|
chapters: ChapterEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface KbMeta extends BookMeta {
|
||||||
|
totalChapters: number;
|
||||||
|
description: string;
|
||||||
|
volumes: { number: number; totalChapters: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
interface SbMeta extends BookMeta {
|
interface SbMeta extends BookMeta {
|
||||||
totalCantos: number;
|
totalCantos: number;
|
||||||
cantos: CantoEntry[];
|
cantos: CantoEntry[];
|
||||||
@@ -112,6 +118,16 @@ function buildBookList(allBooks: BookMeta[]): BookInfo[] {
|
|||||||
totalVerses,
|
totalVerses,
|
||||||
href: "/books/cc",
|
href: "/books/cc",
|
||||||
});
|
});
|
||||||
|
} else if (slug === "kb") {
|
||||||
|
const kb = data as KbMeta;
|
||||||
|
books.push({
|
||||||
|
slug,
|
||||||
|
title,
|
||||||
|
author,
|
||||||
|
description: `${kb.totalChapters} chapters · 2 volumes`,
|
||||||
|
totalVerses: 0,
|
||||||
|
href: "/books/kb",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,11 +172,13 @@ export default async function BooksPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center gap-3 text-sm">
|
<div className="flex items-center gap-3 text-sm">
|
||||||
<Badge variant="secondary">{book.description}</Badge>
|
<Badge variant="secondary">{book.description}</Badge>
|
||||||
|
{book.totalVerses > 0 && (
|
||||||
<ExtractedVerseCount
|
<ExtractedVerseCount
|
||||||
count={book.totalVerses}
|
count={book.totalVerses}
|
||||||
label="extracted verses"
|
label="extracted verses"
|
||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
+129
-1
@@ -3,10 +3,11 @@
|
|||||||
*
|
*
|
||||||
* These functions query MongoDB via Prisma and are called directly by
|
* These functions query MongoDB via Prisma and are called directly by
|
||||||
* Next.js server components. The /api/* routes exist for external consumers
|
* Next.js server components. The /api/* routes exist for external consumers
|
||||||
* and for client-side infinite-scroll fetching.
|
* and for client-side verse-navigation fetching.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
|
import type { JsonValue } from "@prisma/client/runtime/library";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
@@ -56,6 +57,22 @@ export type GlossaryRecord = {
|
|||||||
definitionHtml: string;
|
definitionHtml: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Prose chapter types (for KB and other prose books)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type ProseChapterRecord = {
|
||||||
|
id: string;
|
||||||
|
book: string;
|
||||||
|
volume: number;
|
||||||
|
chapter: number | null;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
slug: string;
|
||||||
|
order: number;
|
||||||
|
paragraphs: string[];
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Book meta
|
// Book meta
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -174,6 +191,117 @@ export async function getSingleVerse(
|
|||||||
return result as unknown as VerseRecord | null;
|
return result as unknown as VerseRecord | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Prose chapters (KB and other prose books)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all prose chapters/segments for a given book and volume,
|
||||||
|
* optionally filtered by type ("chapter" | "intro-segment").
|
||||||
|
* Ordered by the `order` field.
|
||||||
|
*/
|
||||||
|
export async function getProseChapters(
|
||||||
|
book: string,
|
||||||
|
volume: number,
|
||||||
|
type?: string,
|
||||||
|
): Promise<ProseChapterRecord[]> {
|
||||||
|
const where: Record<string, unknown> = { book, volume };
|
||||||
|
if (type) where.type = type;
|
||||||
|
|
||||||
|
const results = await prisma.proseChapter.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: { order: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return results.map(normalizeProse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single prose chapter by book + volume + slug.
|
||||||
|
*/
|
||||||
|
export async function getProseChapter(
|
||||||
|
book: string,
|
||||||
|
volume: number,
|
||||||
|
slug: string,
|
||||||
|
): Promise<ProseChapterRecord | null> {
|
||||||
|
const result = await prisma.proseChapter.findUnique({
|
||||||
|
where: {
|
||||||
|
book_volume_slug: { book, volume, slug },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return result ? normalizeProse(result) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a prose chapter from the DB.
|
||||||
|
* Some chapters were stored with paragraphs as a JSON string (fallback
|
||||||
|
* for BSON wire-protocol corruption). This normalizes them back to arrays.
|
||||||
|
*/
|
||||||
|
function normalizeProse(doc: {
|
||||||
|
id: string;
|
||||||
|
book: string;
|
||||||
|
volume: number;
|
||||||
|
chapter: number | null;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
slug: string;
|
||||||
|
order: number;
|
||||||
|
paragraphs: string[] | JsonValue;
|
||||||
|
}): ProseChapterRecord {
|
||||||
|
let paragraphs: string[];
|
||||||
|
|
||||||
|
if (typeof doc.paragraphs === "string") {
|
||||||
|
// Was stored as stringified JSON — parse it back
|
||||||
|
try {
|
||||||
|
paragraphs = JSON.parse(doc.paragraphs);
|
||||||
|
} catch {
|
||||||
|
paragraphs = [doc.paragraphs];
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(doc.paragraphs)) {
|
||||||
|
paragraphs = doc.paragraphs as string[];
|
||||||
|
} else {
|
||||||
|
paragraphs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize HTML — strip dangerous tags and event handlers
|
||||||
|
paragraphs = paragraphs.map(sanitizeHtml);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: doc.id,
|
||||||
|
book: doc.book,
|
||||||
|
volume: doc.volume,
|
||||||
|
chapter: doc.chapter,
|
||||||
|
title: doc.title,
|
||||||
|
type: doc.type,
|
||||||
|
slug: doc.slug,
|
||||||
|
order: doc.order,
|
||||||
|
paragraphs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize an HTML string by stripping dangerous tags and attributes.
|
||||||
|
* Keeps safe formatting tags: <i>, <b>, <em>, <strong>, <br>, <div>, <span>.
|
||||||
|
*/
|
||||||
|
function sanitizeHtml(html: string): string {
|
||||||
|
// Remove <script>...</script>, <style>...</style>, <iframe>...</iframe>
|
||||||
|
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, "");
|
||||||
|
// Remove lone <script>, <style>, <iframe> opening/closing tags
|
||||||
|
clean = clean.replace(
|
||||||
|
/<\/?(?:script|style|iframe|object|embed|form|input|textarea|select|button)[^>]*>/gi,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
// Remove event handler attributes (onclick, onload, onerror, etc.)
|
||||||
|
clean = clean.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
|
||||||
|
// Remove javascript: URLs
|
||||||
|
clean = clean.replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href=""');
|
||||||
|
clean = clean.replace(/href\s*=\s*'javascript:[^']*'/gi, "href=''");
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Glossary
|
// Glossary
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Generated
+28
@@ -8362,6 +8362,34 @@
|
|||||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||||
|
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.0.1",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
|||||||
@@ -63,6 +63,36 @@ model BookMeta {
|
|||||||
@@map("book_meta")
|
@@map("book_meta")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ProseChapter — for prose books like Kṛṣṇa Book (no verse structure)
|
||||||
|
//
|
||||||
|
// book: "kb"
|
||||||
|
// volume: volume number (1 or 2 for KB)
|
||||||
|
// chapter: chapter number within the volume
|
||||||
|
// title: chapter title
|
||||||
|
// type: "chapter" | "intro-segment"
|
||||||
|
// slug: URL-friendly identifier (e.g. "01", "preface", "words-from-apple")
|
||||||
|
// order: sort order (chapter number for chapters, custom for intro segments)
|
||||||
|
// paragraphs: array of HTML paragraph strings
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
model ProseChapter {
|
||||||
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||||
|
book String // "kb"
|
||||||
|
volume Int
|
||||||
|
chapter Int? // null for intro segments
|
||||||
|
title String
|
||||||
|
type String // "chapter" | "intro-segment"
|
||||||
|
slug String
|
||||||
|
order Int
|
||||||
|
paragraphs String[]
|
||||||
|
|
||||||
|
@@unique([book, volume, slug])
|
||||||
|
@@index([book])
|
||||||
|
@@index([book, volume])
|
||||||
|
@@index([book, volume, type])
|
||||||
|
@@map("prose_chapters")
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// GlossaryEntry — individual glossary term + definition
|
// GlossaryEntry — individual glossary term + definition
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract Kṛṣṇa Book introduction and build intro.json with chapter listing.
|
||||||
|
|
||||||
|
Reads public/html/other.html to extract:
|
||||||
|
1. The introduction/preface text (lines ~713–771)
|
||||||
|
2. The full chapter listing from p13 bookmark tags (KB 1970-V-C / Title)
|
||||||
|
3. Chapter end markers ("Thus ends the Bhaktivedanta purport of the Nth Chapter...")
|
||||||
|
|
||||||
|
Output: lib/jsons/books/kb/intro.json
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/kb_extract_intro.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup, Tag
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ScaGoudy → IAST Unicode mapping
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCAGOUDY_TO_IAST = {
|
||||||
|
"ä": "ā",
|
||||||
|
"Ä": "Ā",
|
||||||
|
"å": "ṛ",
|
||||||
|
"Å": "Ṛ",
|
||||||
|
"ñ": "ṣ",
|
||||||
|
"Ñ": "Ṣ",
|
||||||
|
"ö": "ṭ",
|
||||||
|
"Ö": "Ṭ",
|
||||||
|
"ë": "ṇ",
|
||||||
|
"Ë": "Ṇ",
|
||||||
|
"ò": "ḍ",
|
||||||
|
"Ò": "Ḍ",
|
||||||
|
"ç": "ś",
|
||||||
|
"Ç": "Ś",
|
||||||
|
"ì": "ṅ",
|
||||||
|
"ï": "ñ",
|
||||||
|
"ü": "ū",
|
||||||
|
"Ü": "Ū",
|
||||||
|
"é": "ī",
|
||||||
|
"É": "Ī",
|
||||||
|
"à": "ṁ",
|
||||||
|
"ù": "ḥ",
|
||||||
|
"è": "ṝ",
|
||||||
|
"û": "ṁl",
|
||||||
|
"ÿ": "ḷ",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def decode_scagoudy(text: str) -> str:
|
||||||
|
for src, dst in SCAGOUDY_TO_IAST.items():
|
||||||
|
text = text.replace(src, dst)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def decode_html_content(tag: Tag) -> str:
|
||||||
|
inner = "".join(str(child) for child in tag.children)
|
||||||
|
inner = decode_scagoudy(inner)
|
||||||
|
inner = re.sub(r'<span[^>]*class="[^"]*"[^>]*>(.*?)</span>', r"\1", inner)
|
||||||
|
inner = re.sub(r"\s+", " ", inner).strip()
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def decode_text(tag: Tag) -> str:
|
||||||
|
return decode_scagoudy(tag.get_text().strip())
|
||||||
|
|
||||||
|
|
||||||
|
def get_css_class(tag: Tag) -> str:
|
||||||
|
classes = tag.get("class", [])
|
||||||
|
return classes[0] if classes else ""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Extract introduction paragraphs (lines ~713–771)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
# CSS classes used for Sanskrit verse pādas in the introduction
|
||||||
|
VERSE_PADA_CLASSES = frozenset(["p4", "p5", "p10"])
|
||||||
|
|
||||||
|
|
||||||
|
def extract_introduction(soup: BeautifulSoup) -> list[str]:
|
||||||
|
"""Extract the Kṛṣṇa Book introduction/preface paragraphs.
|
||||||
|
|
||||||
|
Starts at the 'KṚṢṆA BOOK-Introduction' header and ends at the
|
||||||
|
'February 26th, 1970' date line (first occurrence — the undated
|
||||||
|
typed document version).
|
||||||
|
|
||||||
|
Sanskrit verse lines (p4/p5 classes) are grouped into a single
|
||||||
|
center-aligned italic block.
|
||||||
|
"""
|
||||||
|
paragraphs = soup.find_all("p")
|
||||||
|
intro_paragraphs: list[str] = []
|
||||||
|
in_intro = False
|
||||||
|
found_date = False
|
||||||
|
verse_accumulator: list[str] = []
|
||||||
|
|
||||||
|
def flush_verse():
|
||||||
|
"""Join accumulated Sanskrit verse pādas into one formatted block."""
|
||||||
|
if verse_accumulator:
|
||||||
|
joined = "<br>".join(verse_accumulator)
|
||||||
|
intro_paragraphs.append(
|
||||||
|
f'<div style="text-align:center"><i>{joined}</i></div>'
|
||||||
|
)
|
||||||
|
verse_accumulator.clear()
|
||||||
|
|
||||||
|
for p in paragraphs:
|
||||||
|
cls = get_css_class(p)
|
||||||
|
text = p.get_text().strip()
|
||||||
|
|
||||||
|
# Start marker
|
||||||
|
if not in_intro:
|
||||||
|
if "BOOK-Introduction" in text or "BOOK\u2011Introduction" in text:
|
||||||
|
in_intro = True
|
||||||
|
continue
|
||||||
|
# Also match the ScaGoudy-encoded version
|
||||||
|
if "BOOK" in text and "Introduction" in text and "K" in text:
|
||||||
|
decoded = decode_text(p)
|
||||||
|
if "KṚṢṆA" in decoded and "Introduction" in decoded:
|
||||||
|
in_intro = True
|
||||||
|
continue
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip the "Preface" subheader
|
||||||
|
if text == "Preface" or text.startswith("*****"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# End marker — "February 26th, 1970" (first occurrence)
|
||||||
|
if "February 26th, 1970" in text:
|
||||||
|
flush_verse()
|
||||||
|
found_date = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Skip empty paragraphs
|
||||||
|
if not text or text == "\xa0":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sanskrit verse pādas (p4, p5, p10) — accumulate into a single block
|
||||||
|
# Use decode_html_content to preserve <br> tags within multi-line verse elements
|
||||||
|
if cls in VERSE_PADA_CLASSES:
|
||||||
|
html = decode_html_content(p)
|
||||||
|
if html:
|
||||||
|
verse_accumulator.append(html)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we were accumulating verse lines and hit a non-verse paragraph,
|
||||||
|
# flush the verse block first
|
||||||
|
flush_verse()
|
||||||
|
|
||||||
|
# Collect the paragraph
|
||||||
|
html = decode_html_content(p)
|
||||||
|
if html:
|
||||||
|
intro_paragraphs.append(html)
|
||||||
|
|
||||||
|
if not found_date:
|
||||||
|
print(" WARNING: Did not find the end marker 'February 26th, 1970'")
|
||||||
|
|
||||||
|
return intro_paragraphs
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Extract chapter listing from p13 bookmarks
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def extract_chapters(soup: BeautifulSoup) -> list[dict]:
|
||||||
|
"""Extract the full chapter listing from p13 bookmark tags.
|
||||||
|
|
||||||
|
Format: 'KB 1970-V-C / Title' where V=volume, C=chapter within volume.
|
||||||
|
We produce a flat chapter numbering (1–89) across both volumes.
|
||||||
|
|
||||||
|
Also finds chapter end markers to count paragraphs per chapter
|
||||||
|
(useful for later extraction).
|
||||||
|
"""
|
||||||
|
chapters: list[dict] = []
|
||||||
|
chapter_num = 0
|
||||||
|
|
||||||
|
# Also extract chapter end markers for titles
|
||||||
|
end_titles: dict[int, str] = {}
|
||||||
|
for p in soup.find_all("p"):
|
||||||
|
cls = get_css_class(p)
|
||||||
|
text = decode_text(p)
|
||||||
|
# "Thus ends the Bhaktivedanta purport of the First Chapter of Kṛṣṇa, "Title""
|
||||||
|
if "Thus ends" in text and "Chapter of" in text and "Kṛṣṇa" in text:
|
||||||
|
# Extract chapter ordinal and title
|
||||||
|
m = re.search(
|
||||||
|
r"(?:the\s+)?(\w+)\s+Chapter\s+of\s+Kṛṣṇa,\s*[\"\"\"]*(.+?)[\"\"\"]*\.?\s*$",
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
if m:
|
||||||
|
# We don't need the ordinal — just the title for cross-referencing
|
||||||
|
title_from_end = m.group(2).strip().rstrip(".")
|
||||||
|
# Store by the chapter count we've seen so far (rough approximation)
|
||||||
|
|
||||||
|
# Extract from p13 bookmarks
|
||||||
|
for p in soup.find_all("p"):
|
||||||
|
cls = get_css_class(p)
|
||||||
|
if cls != "p13":
|
||||||
|
continue
|
||||||
|
|
||||||
|
text = decode_text(p)
|
||||||
|
|
||||||
|
# Match: KB 1970-V-C / Title
|
||||||
|
m = re.match(r"KB\s+1970-(\d+)-(\d+)\s*/\s*(.+)", text)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
|
||||||
|
volume = int(m.group(1))
|
||||||
|
chapter_in_vol = int(m.group(2))
|
||||||
|
title = m.group(3).strip()
|
||||||
|
|
||||||
|
chapter_num += 1
|
||||||
|
|
||||||
|
chapters.append(
|
||||||
|
{
|
||||||
|
"number": chapter_num,
|
||||||
|
"volume": volume,
|
||||||
|
"chapterInVolume": chapter_in_vol,
|
||||||
|
"title": title,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return chapters
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
html_path = os.path.join(base_dir, "public", "html", "other.html")
|
||||||
|
output_path = os.path.join(base_dir, "lib", "jsons", "books", "kb", "intro.json")
|
||||||
|
|
||||||
|
if not os.path.exists(html_path):
|
||||||
|
print(f"ERROR: {html_path} not found!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Reading {html_path}...")
|
||||||
|
with open(html_path, "r", encoding="utf-8") as f:
|
||||||
|
soup = BeautifulSoup(f, "html.parser")
|
||||||
|
|
||||||
|
# Extract introduction
|
||||||
|
print("Extracting introduction...")
|
||||||
|
intro_paragraphs = extract_introduction(soup)
|
||||||
|
print(f" Found {len(intro_paragraphs)} introduction paragraphs")
|
||||||
|
|
||||||
|
# Extract chapter listing
|
||||||
|
print("Extracting chapter listing...")
|
||||||
|
chapters = extract_chapters(soup)
|
||||||
|
print(f" Found {len(chapters)} chapters")
|
||||||
|
|
||||||
|
# Count per volume
|
||||||
|
vol1 = [c for c in chapters if c["volume"] == 1]
|
||||||
|
vol2 = [c for c in chapters if c["volume"] == 2]
|
||||||
|
print(f" Volume 1: {len(vol1)} chapters")
|
||||||
|
print(f" Volume 2: {len(vol2)} chapters")
|
||||||
|
|
||||||
|
# Build intro.json
|
||||||
|
# Strip volume/chapterInVolume from the chapter entries for the public JSON
|
||||||
|
chapters_public = [{"number": c["number"], "title": c["title"]} for c in chapters]
|
||||||
|
|
||||||
|
intro = {
|
||||||
|
"book": "kb",
|
||||||
|
"title": "Kṛṣṇa, The Supreme Personality of Godhead",
|
||||||
|
"author": "His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda",
|
||||||
|
"description": "A summary study of the Tenth Canto of Śrīmad-Bhāgavatam. Originally published in two volumes in 1970.",
|
||||||
|
"totalChapters": len(chapters),
|
||||||
|
"volumes": [
|
||||||
|
{"number": 1, "totalChapters": len(vol1)},
|
||||||
|
{"number": 2, "totalChapters": len(vol2)},
|
||||||
|
],
|
||||||
|
"chapters": chapters_public,
|
||||||
|
"introduction": intro_paragraphs,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write
|
||||||
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(intro, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"\nWritten: {output_path}")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n--- Summary ---")
|
||||||
|
print(f"Title: {intro['title']}")
|
||||||
|
print(f"Chapters: {intro['totalChapters']}")
|
||||||
|
print(f"Introduction paragraphs: {len(intro_paragraphs)}")
|
||||||
|
|
||||||
|
# Show first few intro paragraphs
|
||||||
|
print(f"\n--- Introduction (first 3 paragraphs) ---")
|
||||||
|
for i, para in enumerate(intro_paragraphs[:3]):
|
||||||
|
print(f" [{i + 1}] {para[:120]}...")
|
||||||
|
|
||||||
|
# Show first and last chapters
|
||||||
|
print(f"\n--- Chapters ---")
|
||||||
|
for c in chapters[:5]:
|
||||||
|
print(f" Ch {c['number']:>2} (Vol {c['volume']}): {c['title']}")
|
||||||
|
print(f" ...")
|
||||||
|
for c in chapters[-3:]:
|
||||||
|
print(f" Ch {c['number']:>2} (Vol {c['volume']}): {c['title']}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract Kṛṣṇa Book Volume 1 chapters from other.html.
|
||||||
|
|
||||||
|
Volume 1 has 54 chapters (KB 1970-1-1 through KB 1970-1-54).
|
||||||
|
Each chapter is a prose chapter (no verses) — just paragraphs of purport text.
|
||||||
|
|
||||||
|
The chapters are delimited by p13 bookmark tags: "KB 1970-1-N / Title"
|
||||||
|
Each chapter starts with a p27 heading: "N /Title"
|
||||||
|
Body paragraphs are in p6 (primary) and p28 classes.
|
||||||
|
Chapter end markers: "Thus ends the Bhaktivedanta purport of the Nth Chapter..."
|
||||||
|
|
||||||
|
Output schema (one JSON per chapter):
|
||||||
|
{
|
||||||
|
"book": "kb",
|
||||||
|
"volume": 1,
|
||||||
|
"chapter": 1,
|
||||||
|
"title": "Advent of Lord Kṛṣṇa",
|
||||||
|
"type": "chapter",
|
||||||
|
"paragraphs": ["<p>...</p>", ...]
|
||||||
|
}
|
||||||
|
|
||||||
|
Output: lib/jsons/books/kb/vol-1/chapters/01.json through 54.json
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/kb_extract_vol1_chapters.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup, Tag
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ScaGoudy → IAST Unicode mapping
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCAGOUDY_TO_IAST = {
|
||||||
|
"ä": "ā",
|
||||||
|
"Ä": "Ā",
|
||||||
|
"å": "ṛ",
|
||||||
|
"Å": "Ṛ",
|
||||||
|
"ñ": "ṣ",
|
||||||
|
"Ñ": "Ṣ",
|
||||||
|
"ö": "ṭ",
|
||||||
|
"Ö": "Ṭ",
|
||||||
|
"ë": "ṇ",
|
||||||
|
"Ë": "Ṇ",
|
||||||
|
"ò": "ḍ",
|
||||||
|
"Ò": "Ḍ",
|
||||||
|
"ç": "ś",
|
||||||
|
"Ç": "Ś",
|
||||||
|
"ì": "ṅ",
|
||||||
|
"ï": "ñ",
|
||||||
|
"ü": "ū",
|
||||||
|
"Ü": "Ū",
|
||||||
|
"é": "ī",
|
||||||
|
"É": "Ī",
|
||||||
|
"à": "ṁ",
|
||||||
|
"ù": "ḥ",
|
||||||
|
"è": "ṝ",
|
||||||
|
"û": "ṁl",
|
||||||
|
"ÿ": "ḷ",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def decode_scagoudy(text: str) -> str:
|
||||||
|
for src, dst in SCAGOUDY_TO_IAST.items():
|
||||||
|
text = text.replace(src, dst)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def decode_html_content(tag: Tag) -> str:
|
||||||
|
"""Extract inner HTML from a tag, decode ScaGoudy, preserve formatting tags."""
|
||||||
|
inner = "".join(str(child) for child in tag.children)
|
||||||
|
inner = decode_scagoudy(inner)
|
||||||
|
# Remove span tags with CSS classes (RTF conversion artifacts)
|
||||||
|
inner = re.sub(r'<span[^>]*class="[^"]*"[^>]*>(.*?)</span>', r"\1", inner)
|
||||||
|
inner = re.sub(r"\s+", " ", inner).strip()
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def decode_text(tag: Tag) -> str:
|
||||||
|
return decode_scagoudy(tag.get_text().strip())
|
||||||
|
|
||||||
|
|
||||||
|
def get_css_class(tag: Tag) -> str:
|
||||||
|
classes = tag.get("class", [])
|
||||||
|
return classes[0] if classes else ""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CSS classes for Sanskrit verse pādas (center-aligned italic blocks
|
||||||
|
# that sometimes appear within chapter prose — e.g., quoted verses)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
VERSE_PADA_CLASSES = frozenset(["p4", "p5", "p10", "p19", "p24"])
|
||||||
|
|
||||||
|
# Primary body paragraph classes
|
||||||
|
BODY_CLASSES = frozenset(["p6", "p28", "p23"])
|
||||||
|
|
||||||
|
# Chapter heading class (bold title like "1 /Advent of Lord Kṛṣṇa")
|
||||||
|
HEADING_CLASS = "p27"
|
||||||
|
|
||||||
|
# Bookmark class
|
||||||
|
BOOKMARK_CLASS = "p13"
|
||||||
|
|
||||||
|
# Volume header class
|
||||||
|
VOLUME_HEADER_CLASS = "p12"
|
||||||
|
|
||||||
|
# Classes to skip entirely
|
||||||
|
SKIP_CLASSES = frozenset(
|
||||||
|
[
|
||||||
|
"p9", # empty / decorative breaks
|
||||||
|
"p11", # empty breaks
|
||||||
|
"p15", # empty breaks
|
||||||
|
"p17", # empty breaks
|
||||||
|
"p20", # empty breaks
|
||||||
|
"p14", # section labels like "Flap:"
|
||||||
|
"p16", # "Not for sale in India"
|
||||||
|
"p25", # handwritten notes marker
|
||||||
|
"p26", # Apple Corps header
|
||||||
|
"p29", # image descriptions (color plate references)
|
||||||
|
"p30", # image descriptions
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_empty(tag: Tag) -> bool:
|
||||||
|
text = tag.get_text().strip()
|
||||||
|
return not text or text == "\xa0"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Extraction
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def extract_vol1_chapters(soup: BeautifulSoup):
|
||||||
|
"""Extract all 54 chapters of Volume 1.
|
||||||
|
|
||||||
|
Returns a list of chapter dicts.
|
||||||
|
"""
|
||||||
|
all_paragraphs = list(soup.find_all("p"))
|
||||||
|
|
||||||
|
# Step 1: Find all Vol 1 chapter bookmark positions
|
||||||
|
# Format: "KB 1970-1-N / Title"
|
||||||
|
chapter_bookmarks = [] # (paragraph_index, chapter_num, title)
|
||||||
|
|
||||||
|
for i, p in enumerate(all_paragraphs):
|
||||||
|
cls = get_css_class(p)
|
||||||
|
if cls != BOOKMARK_CLASS:
|
||||||
|
continue
|
||||||
|
text = decode_text(p)
|
||||||
|
m = re.match(r"KB\s+1970-1-(\d+)\s*/\s*(.+)", text)
|
||||||
|
if m:
|
||||||
|
ch_num = int(m.group(1))
|
||||||
|
title = m.group(2).strip()
|
||||||
|
chapter_bookmarks.append((i, ch_num, title))
|
||||||
|
|
||||||
|
if not chapter_bookmarks:
|
||||||
|
print("ERROR: No Vol 1 chapter bookmarks found!")
|
||||||
|
return []
|
||||||
|
|
||||||
|
print(f" Found {len(chapter_bookmarks)} Vol 1 chapter bookmarks")
|
||||||
|
|
||||||
|
# Step 2: Find the end boundary (Volume 2 header or end of file)
|
||||||
|
vol2_start = len(all_paragraphs)
|
||||||
|
for i, p in enumerate(all_paragraphs):
|
||||||
|
cls = get_css_class(p)
|
||||||
|
if cls == VOLUME_HEADER_CLASS:
|
||||||
|
text = decode_text(p)
|
||||||
|
if "Volume 2" in text:
|
||||||
|
vol2_start = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Step 3: Extract each chapter
|
||||||
|
chapters = []
|
||||||
|
|
||||||
|
for idx, (bookmark_pos, ch_num, title) in enumerate(chapter_bookmarks):
|
||||||
|
# Chapter content starts after the bookmark
|
||||||
|
content_start = bookmark_pos + 1
|
||||||
|
|
||||||
|
# Chapter content ends at the next chapter's bookmark (or Vol 2 boundary)
|
||||||
|
if idx + 1 < len(chapter_bookmarks):
|
||||||
|
content_end = chapter_bookmarks[idx + 1][0]
|
||||||
|
else:
|
||||||
|
content_end = vol2_start
|
||||||
|
|
||||||
|
# Extract paragraphs for this chapter
|
||||||
|
paragraphs = extract_chapter_paragraphs(
|
||||||
|
all_paragraphs, content_start, content_end
|
||||||
|
)
|
||||||
|
|
||||||
|
chapters.append(
|
||||||
|
{
|
||||||
|
"book": "kb",
|
||||||
|
"volume": 1,
|
||||||
|
"chapter": ch_num,
|
||||||
|
"title": title,
|
||||||
|
"type": "chapter",
|
||||||
|
"paragraphs": paragraphs,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return chapters
|
||||||
|
|
||||||
|
|
||||||
|
def extract_chapter_paragraphs(
|
||||||
|
all_paragraphs: list,
|
||||||
|
start_idx: int,
|
||||||
|
end_idx: int,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Extract the prose paragraphs for a single chapter.
|
||||||
|
|
||||||
|
Handles:
|
||||||
|
- Regular body paragraphs (p6, p28)
|
||||||
|
- Quoted Sanskrit verse blocks (p4, p5, p19, p24) → grouped into centered italic
|
||||||
|
- Chapter heading (p27) → included as first paragraph
|
||||||
|
- Chapter end marker ("Thus ends...") → included as last paragraph (italic)
|
||||||
|
- Skips empty/decorative paragraphs
|
||||||
|
"""
|
||||||
|
result: list[str] = []
|
||||||
|
verse_accumulator: list[str] = []
|
||||||
|
|
||||||
|
def flush_verse():
|
||||||
|
if verse_accumulator:
|
||||||
|
joined = "<br>".join(verse_accumulator)
|
||||||
|
result.append(f'<div style="text-align:center"><i>{joined}</i></div>')
|
||||||
|
verse_accumulator.clear()
|
||||||
|
|
||||||
|
for i in range(start_idx, end_idx):
|
||||||
|
p = all_paragraphs[i]
|
||||||
|
cls = get_css_class(p)
|
||||||
|
|
||||||
|
# Skip empty paragraphs
|
||||||
|
if is_empty(p):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip noise classes
|
||||||
|
if cls in SKIP_CLASSES:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip bookmark tags that might be inside the range
|
||||||
|
if cls == BOOKMARK_CLASS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip volume headers
|
||||||
|
if cls == VOLUME_HEADER_CLASS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sanskrit verse pādas — accumulate into centered block
|
||||||
|
# Use decode_html_content to preserve <br> tags within multi-line verse elements
|
||||||
|
if cls in VERSE_PADA_CLASSES:
|
||||||
|
html = decode_html_content(p)
|
||||||
|
if html:
|
||||||
|
verse_accumulator.append(html)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Non-verse paragraph — flush any accumulated verse first
|
||||||
|
flush_verse()
|
||||||
|
|
||||||
|
# Chapter heading (p27) — keep it, it's the chapter title
|
||||||
|
if cls == HEADING_CLASS:
|
||||||
|
html = decode_html_content(p)
|
||||||
|
if html:
|
||||||
|
result.append(html)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Body paragraphs and everything else
|
||||||
|
html = decode_html_content(p)
|
||||||
|
if html:
|
||||||
|
result.append(html)
|
||||||
|
|
||||||
|
# Flush any trailing verse
|
||||||
|
flush_verse()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Output
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(data, filepath):
|
||||||
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
html_path = os.path.join(base_dir, "public", "html", "other.html")
|
||||||
|
output_dir = os.path.join(
|
||||||
|
base_dir, "lib", "jsons", "books", "kb", "vol-1", "chapters"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(html_path):
|
||||||
|
print(f"ERROR: {html_path} not found!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Reading {html_path}...")
|
||||||
|
with open(html_path, "r", encoding="utf-8") as f:
|
||||||
|
soup = BeautifulSoup(f, "html.parser")
|
||||||
|
|
||||||
|
print("Extracting Volume 1 chapters...")
|
||||||
|
chapters = extract_vol1_chapters(soup)
|
||||||
|
|
||||||
|
if not chapters:
|
||||||
|
print("ERROR: No chapters extracted!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"\nExtracted {len(chapters)} chapters\n")
|
||||||
|
|
||||||
|
# Write each chapter
|
||||||
|
for ch in chapters:
|
||||||
|
ch_num = str(ch["chapter"]).zfill(2)
|
||||||
|
filepath = os.path.join(output_dir, f"{ch_num}.json")
|
||||||
|
write_json(ch, filepath)
|
||||||
|
print(
|
||||||
|
f" Ch {ch['chapter']:>2}: {len(ch['paragraphs']):>3} paragraphs — {ch['title']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
total_paragraphs = sum(len(ch["paragraphs"]) for ch in chapters)
|
||||||
|
print(f"\n--- Summary ---")
|
||||||
|
print(f"Total chapters: {len(chapters)}")
|
||||||
|
print(f"Total paragraphs: {total_paragraphs}")
|
||||||
|
print(f"Average paragraphs per chapter: {total_paragraphs / len(chapters):.1f}")
|
||||||
|
print(f"Output: {output_dir}/")
|
||||||
|
|
||||||
|
# Show samples from first and last chapter
|
||||||
|
print(f"\n--- Sample: Chapter 1 ---")
|
||||||
|
ch1 = chapters[0]
|
||||||
|
for i, p in enumerate(ch1["paragraphs"][:3]):
|
||||||
|
preview = re.sub(r"<[^>]+>", "", p)[:100]
|
||||||
|
print(f" [{i}] {preview}...")
|
||||||
|
|
||||||
|
print(f"\n--- Sample: Chapter {chapters[-1]['chapter']} ---")
|
||||||
|
last = chapters[-1]
|
||||||
|
for i, p in enumerate(last["paragraphs"][:3]):
|
||||||
|
preview = re.sub(r"<[^>]+>", "", p)[:100]
|
||||||
|
print(f" [{i}] {preview}...")
|
||||||
|
|
||||||
|
# Quality check: verify all chapters have the end marker
|
||||||
|
print(f"\n--- Quality Check ---")
|
||||||
|
missing_end = 0
|
||||||
|
for ch in chapters:
|
||||||
|
has_end = any("Thus end" in p for p in ch["paragraphs"])
|
||||||
|
if not has_end:
|
||||||
|
missing_end += 1
|
||||||
|
print(
|
||||||
|
f" ⚠ Chapter {ch['chapter']} ({ch['title']}) — no 'Thus ends' marker"
|
||||||
|
)
|
||||||
|
if missing_end == 0:
|
||||||
|
print(f" ✅ All {len(chapters)} chapters have 'Thus ends' markers")
|
||||||
|
else:
|
||||||
|
print(f" ⚠ {missing_end} chapters missing 'Thus ends' markers")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract Kṛṣṇa Book Volume 1 introductory segments from other.html.
|
||||||
|
|
||||||
|
The Volume 1 front matter (lines ~772–901) contains 5 segments before Chapter 1:
|
||||||
|
1. Flap intro — book jacket text
|
||||||
|
2. Dedication — "To My Father, Gour Mohon De (1849-1930)"
|
||||||
|
3. Words from Apple — George Harrison's introduction
|
||||||
|
4. Preface — Prabhupāda's preface
|
||||||
|
5. Introduction — Prabhupāda's introduction
|
||||||
|
|
||||||
|
Each segment is extracted into a JSON file with a common schema:
|
||||||
|
{
|
||||||
|
"slug": "flap-intro",
|
||||||
|
"title": "Flap Introduction",
|
||||||
|
"type": "intro-segment",
|
||||||
|
"book": "kb",
|
||||||
|
"volume": 1,
|
||||||
|
"order": 1,
|
||||||
|
"paragraphs": ["<p>...</p>", ...]
|
||||||
|
}
|
||||||
|
|
||||||
|
Output: lib/jsons/books/kb/vol-1/intro/*.json
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/kb_extract_vol1_intro.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup, Tag
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ScaGoudy → IAST Unicode mapping
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCAGOUDY_TO_IAST = {
|
||||||
|
"ä": "ā",
|
||||||
|
"Ä": "Ā",
|
||||||
|
"å": "ṛ",
|
||||||
|
"Å": "Ṛ",
|
||||||
|
"ñ": "ṣ",
|
||||||
|
"Ñ": "Ṣ",
|
||||||
|
"ö": "ṭ",
|
||||||
|
"Ö": "Ṭ",
|
||||||
|
"ë": "ṇ",
|
||||||
|
"Ë": "Ṇ",
|
||||||
|
"ò": "ḍ",
|
||||||
|
"Ò": "Ḍ",
|
||||||
|
"ç": "ś",
|
||||||
|
"Ç": "Ś",
|
||||||
|
"ì": "ṅ",
|
||||||
|
"ï": "ñ",
|
||||||
|
"ü": "ū",
|
||||||
|
"Ü": "Ū",
|
||||||
|
"é": "ī",
|
||||||
|
"É": "Ī",
|
||||||
|
"à": "ṁ",
|
||||||
|
"ù": "ḥ",
|
||||||
|
"è": "ṝ",
|
||||||
|
"û": "ṁl",
|
||||||
|
"ÿ": "ḷ",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def decode_scagoudy(text: str) -> str:
|
||||||
|
for src, dst in SCAGOUDY_TO_IAST.items():
|
||||||
|
text = text.replace(src, dst)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def decode_html_content(tag: Tag) -> str:
|
||||||
|
inner = "".join(str(child) for child in tag.children)
|
||||||
|
inner = decode_scagoudy(inner)
|
||||||
|
inner = re.sub(r'<span[^>]*class="[^"]*"[^>]*>(.*?)</span>', r"\1", inner)
|
||||||
|
inner = re.sub(r"\s+", " ", inner).strip()
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def decode_text(tag: Tag) -> str:
|
||||||
|
return decode_scagoudy(tag.get_text().strip())
|
||||||
|
|
||||||
|
|
||||||
|
def get_css_class(tag: Tag) -> str:
|
||||||
|
classes = tag.get("class", [])
|
||||||
|
return classes[0] if classes else ""
|
||||||
|
|
||||||
|
|
||||||
|
# CSS classes for Sanskrit verse pādas (center-aligned italic)
|
||||||
|
VERSE_PADA_CLASSES = frozenset(["p4", "p5", "p10", "p19", "p24"])
|
||||||
|
|
||||||
|
|
||||||
|
def is_empty(tag: Tag) -> bool:
|
||||||
|
"""Check if a tag is empty / whitespace only."""
|
||||||
|
text = tag.get_text().strip()
|
||||||
|
return not text or text == "\xa0"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Segment definitions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SEGMENTS = [
|
||||||
|
{
|
||||||
|
"slug": "flap-intro",
|
||||||
|
"title": "Flap Introduction",
|
||||||
|
"order": 1,
|
||||||
|
"bookmark": "Kb 1970 /Flap intro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "dedication",
|
||||||
|
"title": "Dedication — To My Father, Gour Mohon De (1849–1930)",
|
||||||
|
"order": 2,
|
||||||
|
"bookmark": "Kb 1970 / To My Father, Gour Mohon De (1849-1930)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "words-from-apple",
|
||||||
|
"title": "Words from Apple — by George Harrison",
|
||||||
|
"order": 3,
|
||||||
|
"bookmark": "Kb 1970 / Words from Apple",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "preface",
|
||||||
|
"title": "Preface",
|
||||||
|
"order": 4,
|
||||||
|
"bookmark": "Kb 1970 / Preface",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "introduction",
|
||||||
|
"title": "Introduction",
|
||||||
|
"order": 5,
|
||||||
|
"bookmark": "Kb 1970 / Introduction",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Extraction
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def find_segment_ranges(soup: BeautifulSoup):
|
||||||
|
"""Find the start/end paragraph indices for each segment.
|
||||||
|
|
||||||
|
Segments are delimited by p13 bookmark tags. The first chapter marker
|
||||||
|
(KB 1970-1-1) ends the last intro segment.
|
||||||
|
"""
|
||||||
|
all_paragraphs = list(soup.find_all("p"))
|
||||||
|
|
||||||
|
# Find all p13 bookmark indices within the KB volume 1 area
|
||||||
|
bookmark_positions = [] # (index, text)
|
||||||
|
for i, p in enumerate(all_paragraphs):
|
||||||
|
cls = get_css_class(p)
|
||||||
|
if cls == "p13":
|
||||||
|
text = decode_text(p)
|
||||||
|
bookmark_positions.append((i, text))
|
||||||
|
|
||||||
|
# Map each segment to its start/end paragraph indices
|
||||||
|
ranges = {}
|
||||||
|
for seg in SEGMENTS:
|
||||||
|
# Find the bookmark that starts this segment
|
||||||
|
start_idx = None
|
||||||
|
for i, (pos, text) in enumerate(bookmark_positions):
|
||||||
|
if seg["bookmark"] in text:
|
||||||
|
# Content starts AFTER the bookmark tag
|
||||||
|
start_idx = pos + 1
|
||||||
|
# End is the next bookmark
|
||||||
|
if i + 1 < len(bookmark_positions):
|
||||||
|
end_idx = bookmark_positions[i + 1][0]
|
||||||
|
else:
|
||||||
|
end_idx = len(all_paragraphs)
|
||||||
|
ranges[seg["slug"]] = (start_idx, end_idx, all_paragraphs)
|
||||||
|
break
|
||||||
|
|
||||||
|
if start_idx is None:
|
||||||
|
print(
|
||||||
|
f" WARNING: Could not find bookmark for '{seg['slug']}': '{seg['bookmark']}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ranges
|
||||||
|
|
||||||
|
|
||||||
|
def extract_segment_paragraphs(
|
||||||
|
all_paragraphs: list,
|
||||||
|
start_idx: int,
|
||||||
|
end_idx: int,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Extract paragraphs for a single segment, handling verse grouping."""
|
||||||
|
result: list[str] = []
|
||||||
|
verse_accumulator: list[str] = []
|
||||||
|
|
||||||
|
def flush_verse():
|
||||||
|
if verse_accumulator:
|
||||||
|
joined = "<br>".join(verse_accumulator)
|
||||||
|
result.append(f'<div style="text-align:center"><i>{joined}</i></div>')
|
||||||
|
verse_accumulator.clear()
|
||||||
|
|
||||||
|
for i in range(start_idx, end_idx):
|
||||||
|
p = all_paragraphs[i]
|
||||||
|
cls = get_css_class(p)
|
||||||
|
text = p.get_text().strip()
|
||||||
|
|
||||||
|
# Skip the segment's own heading (p12, p14, p22, p27 bold headers)
|
||||||
|
# These are the display titles like "Flap:", "Words From Apple", "Preface", etc.
|
||||||
|
# We keep them as the first paragraph if they have meaningful content
|
||||||
|
if cls in ("p12", "p14"):
|
||||||
|
# Volume header or section label — skip
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip empty paragraphs
|
||||||
|
if is_empty(p):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip bookmark tags (p13) that might be nested
|
||||||
|
if cls == "p13":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sanskrit verse pādas — accumulate
|
||||||
|
# Use decode_html_content to preserve <br> tags within multi-line verse elements
|
||||||
|
if cls in VERSE_PADA_CLASSES:
|
||||||
|
html = decode_html_content(p)
|
||||||
|
if html:
|
||||||
|
verse_accumulator.append(html)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Non-verse paragraph — flush any accumulated verse first
|
||||||
|
flush_verse()
|
||||||
|
|
||||||
|
# Extract the HTML content
|
||||||
|
html = decode_html_content(p)
|
||||||
|
if html:
|
||||||
|
result.append(html)
|
||||||
|
|
||||||
|
# Flush any trailing verse
|
||||||
|
flush_verse()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Output
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(data, filepath):
|
||||||
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f" Written: {filepath}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
html_path = os.path.join(base_dir, "public", "html", "other.html")
|
||||||
|
output_dir = os.path.join(base_dir, "lib", "jsons", "books", "kb", "vol-1", "intro")
|
||||||
|
|
||||||
|
if not os.path.exists(html_path):
|
||||||
|
print(f"ERROR: {html_path} not found!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Reading {html_path}...")
|
||||||
|
with open(html_path, "r", encoding="utf-8") as f:
|
||||||
|
soup = BeautifulSoup(f, "html.parser")
|
||||||
|
|
||||||
|
print("Finding segment boundaries...")
|
||||||
|
ranges = find_segment_ranges(soup)
|
||||||
|
|
||||||
|
print(f"Extracting {len(SEGMENTS)} introductory segments...\n")
|
||||||
|
|
||||||
|
for seg in SEGMENTS:
|
||||||
|
slug = seg["slug"]
|
||||||
|
if slug not in ranges:
|
||||||
|
print(f" ⏭ Skipping '{slug}' — bookmark not found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
start_idx, end_idx, all_paragraphs = ranges[slug]
|
||||||
|
paragraphs = extract_segment_paragraphs(all_paragraphs, start_idx, end_idx)
|
||||||
|
|
||||||
|
doc = {
|
||||||
|
"slug": slug,
|
||||||
|
"title": seg["title"],
|
||||||
|
"type": "intro-segment",
|
||||||
|
"book": "kb",
|
||||||
|
"volume": 1,
|
||||||
|
"order": seg["order"],
|
||||||
|
"paragraphs": paragraphs,
|
||||||
|
}
|
||||||
|
|
||||||
|
filepath = os.path.join(output_dir, f"{slug}.json")
|
||||||
|
write_json(doc, filepath)
|
||||||
|
print(f" {seg['title']}: {len(paragraphs)} paragraphs")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n--- Summary ---")
|
||||||
|
for seg in SEGMENTS:
|
||||||
|
slug = seg["slug"]
|
||||||
|
filepath = os.path.join(output_dir, f"{slug}.json")
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
doc = json.load(f)
|
||||||
|
# Show first paragraph preview
|
||||||
|
first = doc["paragraphs"][0] if doc["paragraphs"] else "(empty)"
|
||||||
|
# Strip HTML tags for preview
|
||||||
|
preview = re.sub(r"<[^>]+>", "", first)[:80]
|
||||||
|
print(f" {seg['order']}. {seg['title']}")
|
||||||
|
print(f' {len(doc["paragraphs"])} paragraphs — "{preview}..."')
|
||||||
|
else:
|
||||||
|
print(f" {seg['order']}. {seg['title']} — NOT EXTRACTED")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract Kṛṣṇa Book Volume 2 chapters from other.html.
|
||||||
|
|
||||||
|
Volume 2 has 35 chapters (KB 1970-2-1 through KB 1970-2-35).
|
||||||
|
Each chapter is a prose chapter (no verses) — just paragraphs of purport text.
|
||||||
|
|
||||||
|
The chapters are delimited by p13 bookmark tags: "KB 1970-2-N / Title"
|
||||||
|
Each chapter starts with a p27 heading: "N /Title"
|
||||||
|
Body paragraphs are in p6 (primary) and p28 classes.
|
||||||
|
Chapter end markers: "Thus ends the Bhaktivedanta purport of the Nth Chapter..."
|
||||||
|
|
||||||
|
Output schema (one JSON per chapter):
|
||||||
|
{
|
||||||
|
"book": "kb",
|
||||||
|
"volume": 2,
|
||||||
|
"chapter": 1,
|
||||||
|
"title": "The Story of the Syamantaka Jewel",
|
||||||
|
"type": "chapter",
|
||||||
|
"paragraphs": ["<p>...</p>", ...]
|
||||||
|
}
|
||||||
|
|
||||||
|
Output: lib/jsons/books/kb/vol-2/chapters/01.json through 35.json
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/kb_extract_vol2_chapters.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup, Tag
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ScaGoudy → IAST Unicode mapping
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCAGOUDY_TO_IAST = {
|
||||||
|
"ä": "ā",
|
||||||
|
"Ä": "Ā",
|
||||||
|
"å": "ṛ",
|
||||||
|
"Å": "Ṛ",
|
||||||
|
"ñ": "ṣ",
|
||||||
|
"Ñ": "Ṣ",
|
||||||
|
"ö": "ṭ",
|
||||||
|
"Ö": "Ṭ",
|
||||||
|
"ë": "ṇ",
|
||||||
|
"Ë": "Ṇ",
|
||||||
|
"ò": "ḍ",
|
||||||
|
"Ò": "Ḍ",
|
||||||
|
"ç": "ś",
|
||||||
|
"Ç": "Ś",
|
||||||
|
"ì": "ṅ",
|
||||||
|
"ï": "ñ",
|
||||||
|
"ü": "ū",
|
||||||
|
"Ü": "Ū",
|
||||||
|
"é": "ī",
|
||||||
|
"É": "Ī",
|
||||||
|
"à": "ṁ",
|
||||||
|
"ù": "ḥ",
|
||||||
|
"è": "ṝ",
|
||||||
|
"û": "ṁl",
|
||||||
|
"ÿ": "ḷ",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def decode_scagoudy(text: str) -> str:
|
||||||
|
for src, dst in SCAGOUDY_TO_IAST.items():
|
||||||
|
text = text.replace(src, dst)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def decode_html_content(tag: Tag) -> str:
|
||||||
|
"""Extract inner HTML from a tag, decode ScaGoudy, preserve formatting tags."""
|
||||||
|
inner = "".join(str(child) for child in tag.children)
|
||||||
|
inner = decode_scagoudy(inner)
|
||||||
|
# Remove span tags with CSS classes (RTF conversion artifacts)
|
||||||
|
inner = re.sub(r'<span[^>]*class="[^"]*"[^>]*>(.*?)</span>', r"\1", inner)
|
||||||
|
inner = re.sub(r"\s+", " ", inner).strip()
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def decode_text(tag: Tag) -> str:
|
||||||
|
return decode_scagoudy(tag.get_text().strip())
|
||||||
|
|
||||||
|
|
||||||
|
def get_css_class(tag: Tag) -> str:
|
||||||
|
classes = tag.get("class", [])
|
||||||
|
return classes[0] if classes else ""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CSS classes for Sanskrit verse pādas (center-aligned italic blocks
|
||||||
|
# that sometimes appear within chapter prose — e.g., quoted verses)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
VERSE_PADA_CLASSES = frozenset(["p4", "p5", "p10", "p19", "p24"])
|
||||||
|
|
||||||
|
# Primary body paragraph classes
|
||||||
|
BODY_CLASSES = frozenset(["p6", "p28", "p23"])
|
||||||
|
|
||||||
|
# Chapter heading class (bold title like "1 /The Story of the Syamantaka Jewel")
|
||||||
|
HEADING_CLASS = "p27"
|
||||||
|
|
||||||
|
# Bookmark class
|
||||||
|
BOOKMARK_CLASS = "p13"
|
||||||
|
|
||||||
|
# Volume header class
|
||||||
|
VOLUME_HEADER_CLASS = "p12"
|
||||||
|
|
||||||
|
# Classes to skip entirely
|
||||||
|
SKIP_CLASSES = frozenset(
|
||||||
|
[
|
||||||
|
"p9", # empty / decorative breaks
|
||||||
|
"p11", # empty breaks
|
||||||
|
"p15", # empty breaks
|
||||||
|
"p17", # empty breaks
|
||||||
|
"p20", # empty breaks
|
||||||
|
"p14", # section labels
|
||||||
|
"p16", # "Not for sale in India"
|
||||||
|
"p25", # handwritten notes marker
|
||||||
|
"p26", # Apple Corps header
|
||||||
|
"p29", # image descriptions (color plate references)
|
||||||
|
"p30", # image descriptions
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_empty(tag: Tag) -> bool:
|
||||||
|
text = tag.get_text().strip()
|
||||||
|
return not text or text == "\xa0"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Extraction
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def extract_vol2_chapters(soup: BeautifulSoup):
|
||||||
|
"""Extract all 35 chapters of Volume 2.
|
||||||
|
|
||||||
|
Returns a list of chapter dicts.
|
||||||
|
"""
|
||||||
|
all_paragraphs = list(soup.find_all("p"))
|
||||||
|
|
||||||
|
# Step 1: Find all Vol 2 chapter bookmark positions
|
||||||
|
# Format: "KB 1970-2-N / Title"
|
||||||
|
chapter_bookmarks = [] # (paragraph_index, chapter_num, title)
|
||||||
|
|
||||||
|
for i, p in enumerate(all_paragraphs):
|
||||||
|
cls = get_css_class(p)
|
||||||
|
if cls != BOOKMARK_CLASS:
|
||||||
|
continue
|
||||||
|
text = decode_text(p)
|
||||||
|
m = re.match(r"KB\s+1970-2-(\d+)\s*/\s*(.+)", text)
|
||||||
|
if m:
|
||||||
|
ch_num = int(m.group(1))
|
||||||
|
title = m.group(2).strip()
|
||||||
|
chapter_bookmarks.append((i, ch_num, title))
|
||||||
|
|
||||||
|
if not chapter_bookmarks:
|
||||||
|
print("ERROR: No Vol 2 chapter bookmarks found!")
|
||||||
|
return []
|
||||||
|
|
||||||
|
print(f" Found {len(chapter_bookmarks)} Vol 2 chapter bookmarks")
|
||||||
|
|
||||||
|
# Step 2: Find the end boundary — the next book section or end of file
|
||||||
|
# Look for the next p12 volume header that is NOT KB Vol 2
|
||||||
|
vol2_end = len(all_paragraphs)
|
||||||
|
# Start searching after the last Vol 2 chapter bookmark
|
||||||
|
last_bookmark_pos = chapter_bookmarks[-1][0]
|
||||||
|
for i in range(last_bookmark_pos + 1, len(all_paragraphs)):
|
||||||
|
p = all_paragraphs[i]
|
||||||
|
cls = get_css_class(p)
|
||||||
|
if cls == VOLUME_HEADER_CLASS:
|
||||||
|
text = decode_text(p)
|
||||||
|
# If it's a different book header (not KB), that's our end
|
||||||
|
if "Volume 2" not in text:
|
||||||
|
vol2_end = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Step 3: Extract each chapter
|
||||||
|
chapters = []
|
||||||
|
|
||||||
|
for idx, (bookmark_pos, ch_num, title) in enumerate(chapter_bookmarks):
|
||||||
|
# Chapter content starts after the bookmark
|
||||||
|
content_start = bookmark_pos + 1
|
||||||
|
|
||||||
|
# Chapter content ends at the next chapter's bookmark (or end boundary)
|
||||||
|
if idx + 1 < len(chapter_bookmarks):
|
||||||
|
content_end = chapter_bookmarks[idx + 1][0]
|
||||||
|
else:
|
||||||
|
content_end = vol2_end
|
||||||
|
|
||||||
|
# Extract paragraphs for this chapter
|
||||||
|
paragraphs = extract_chapter_paragraphs(
|
||||||
|
all_paragraphs, content_start, content_end
|
||||||
|
)
|
||||||
|
|
||||||
|
chapters.append(
|
||||||
|
{
|
||||||
|
"book": "kb",
|
||||||
|
"volume": 2,
|
||||||
|
"chapter": ch_num,
|
||||||
|
"title": title,
|
||||||
|
"type": "chapter",
|
||||||
|
"paragraphs": paragraphs,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return chapters
|
||||||
|
|
||||||
|
|
||||||
|
def extract_chapter_paragraphs(
|
||||||
|
all_paragraphs: list,
|
||||||
|
start_idx: int,
|
||||||
|
end_idx: int,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Extract the prose paragraphs for a single chapter.
|
||||||
|
|
||||||
|
Handles:
|
||||||
|
- Regular body paragraphs (p6, p28)
|
||||||
|
- Quoted Sanskrit verse blocks (p4, p5, p19, p24) → grouped into centered italic
|
||||||
|
- Chapter heading (p27) → included as first paragraph
|
||||||
|
- Chapter end marker ("Thus ends...") → included as last paragraph (italic)
|
||||||
|
- Skips empty/decorative paragraphs
|
||||||
|
"""
|
||||||
|
result: list[str] = []
|
||||||
|
verse_accumulator: list[str] = []
|
||||||
|
|
||||||
|
def flush_verse():
|
||||||
|
if verse_accumulator:
|
||||||
|
joined = "<br>".join(verse_accumulator)
|
||||||
|
result.append(f'<div style="text-align:center"><i>{joined}</i></div>')
|
||||||
|
verse_accumulator.clear()
|
||||||
|
|
||||||
|
for i in range(start_idx, end_idx):
|
||||||
|
p = all_paragraphs[i]
|
||||||
|
cls = get_css_class(p)
|
||||||
|
|
||||||
|
# Skip empty paragraphs
|
||||||
|
if is_empty(p):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip noise classes
|
||||||
|
if cls in SKIP_CLASSES:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip bookmark tags that might be inside the range
|
||||||
|
if cls == BOOKMARK_CLASS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip volume headers
|
||||||
|
if cls == VOLUME_HEADER_CLASS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sanskrit verse pādas — accumulate into centered block
|
||||||
|
# Use decode_html_content to preserve <br> tags within multi-line verse elements
|
||||||
|
if cls in VERSE_PADA_CLASSES:
|
||||||
|
html = decode_html_content(p)
|
||||||
|
if html:
|
||||||
|
verse_accumulator.append(html)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Non-verse paragraph — flush any accumulated verse first
|
||||||
|
flush_verse()
|
||||||
|
|
||||||
|
# Chapter heading (p27) — keep it, it's the chapter title
|
||||||
|
if cls == HEADING_CLASS:
|
||||||
|
html = decode_html_content(p)
|
||||||
|
if html:
|
||||||
|
result.append(html)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Body paragraphs and everything else
|
||||||
|
html = decode_html_content(p)
|
||||||
|
if html:
|
||||||
|
result.append(html)
|
||||||
|
|
||||||
|
# Flush any trailing verse
|
||||||
|
flush_verse()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Output
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(data, filepath):
|
||||||
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
html_path = os.path.join(base_dir, "public", "html", "other.html")
|
||||||
|
output_dir = os.path.join(
|
||||||
|
base_dir, "lib", "jsons", "books", "kb", "vol-2", "chapters"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(html_path):
|
||||||
|
print(f"ERROR: {html_path} not found!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Reading {html_path}...")
|
||||||
|
with open(html_path, "r", encoding="utf-8") as f:
|
||||||
|
soup = BeautifulSoup(f, "html.parser")
|
||||||
|
|
||||||
|
print("Extracting Volume 2 chapters...")
|
||||||
|
chapters = extract_vol2_chapters(soup)
|
||||||
|
|
||||||
|
if not chapters:
|
||||||
|
print("ERROR: No chapters extracted!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"\nExtracted {len(chapters)} chapters\n")
|
||||||
|
|
||||||
|
# Write each chapter
|
||||||
|
for ch in chapters:
|
||||||
|
ch_num = str(ch["chapter"]).zfill(2)
|
||||||
|
filepath = os.path.join(output_dir, f"{ch_num}.json")
|
||||||
|
write_json(ch, filepath)
|
||||||
|
print(
|
||||||
|
f" Ch {ch['chapter']:>2}: {len(ch['paragraphs']):>3} paragraphs — {ch['title']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
total_paragraphs = sum(len(ch["paragraphs"]) for ch in chapters)
|
||||||
|
print(f"\n--- Summary ---")
|
||||||
|
print(f"Total chapters: {len(chapters)}")
|
||||||
|
print(f"Total paragraphs: {total_paragraphs}")
|
||||||
|
print(f"Average paragraphs per chapter: {total_paragraphs / len(chapters):.1f}")
|
||||||
|
print(f"Output: {output_dir}/")
|
||||||
|
|
||||||
|
# Show samples from first and last chapter
|
||||||
|
print(f"\n--- Sample: Chapter 1 ---")
|
||||||
|
ch1 = chapters[0]
|
||||||
|
for i, p in enumerate(ch1["paragraphs"][:3]):
|
||||||
|
preview = re.sub(r"<[^>]+>", "", p)[:100]
|
||||||
|
print(f" [{i}] {preview}...")
|
||||||
|
|
||||||
|
print(f"\n--- Sample: Chapter {chapters[-1]['chapter']} ---")
|
||||||
|
last = chapters[-1]
|
||||||
|
for i, p in enumerate(last["paragraphs"][:3]):
|
||||||
|
preview = re.sub(r"<[^>]+>", "", p)[:100]
|
||||||
|
print(f" [{i}] {preview}...")
|
||||||
|
|
||||||
|
# Quality check: verify all chapters have the end marker
|
||||||
|
print(f"\n--- Quality Check ---")
|
||||||
|
missing_end = 0
|
||||||
|
for ch in chapters:
|
||||||
|
has_end = any("Thus end" in p for p in ch["paragraphs"])
|
||||||
|
if not has_end:
|
||||||
|
missing_end += 1
|
||||||
|
print(
|
||||||
|
f" ⚠ Chapter {ch['chapter']} ({ch['title']}) — no 'Thus ends' marker"
|
||||||
|
)
|
||||||
|
if missing_end == 0:
|
||||||
|
print(f" ✅ All {len(chapters)} chapters have 'Thus ends' markers")
|
||||||
|
else:
|
||||||
|
print(f" ⚠ {missing_end} chapters missing 'Thus ends' markers")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+191
-27
@@ -49,6 +49,26 @@ function readAllJsonInDir(dir: string): unknown[] {
|
|||||||
.map((f) => readJson(path.join(dir, f)));
|
.map((f) => readJson(path.join(dir, f)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert documents one at a time with a small delay every N docs.
|
||||||
|
*
|
||||||
|
* Uses insertOne to avoid BSON wire-protocol corruption that occurs
|
||||||
|
* with insertMany on certain MongoDB server/proxy configurations.
|
||||||
|
* A brief pause every 100 docs prevents connection overload.
|
||||||
|
*/
|
||||||
|
async function insertBatched(
|
||||||
|
col: ReturnType<ReturnType<MongoClient["db"]>["collection"]>,
|
||||||
|
docs: Record<string, unknown>[],
|
||||||
|
) {
|
||||||
|
for (let i = 0; i < docs.length; i++) {
|
||||||
|
await col.insertOne(docs[i]);
|
||||||
|
// Pause briefly every 100 docs to give the connection breathing room
|
||||||
|
if ((i + 1) % 100 === 0) {
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -95,6 +115,17 @@ type CcVerse = {
|
|||||||
purport: string[];
|
purport: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ProseChapterJson = {
|
||||||
|
book: string;
|
||||||
|
volume: number;
|
||||||
|
chapter?: number;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
slug?: string;
|
||||||
|
order?: number;
|
||||||
|
paragraphs: string[];
|
||||||
|
};
|
||||||
|
|
||||||
type Sb1962Verse = {
|
type Sb1962Verse = {
|
||||||
book_title: string;
|
book_title: string;
|
||||||
chapter: number;
|
chapter: number;
|
||||||
@@ -178,9 +209,7 @@ async function seedBg(db: ReturnType<MongoClient["db"]>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (docs.length > 0) {
|
await insertBatched(versesCol, docs);
|
||||||
await versesCol.insertMany(docs, { ordered: false });
|
|
||||||
}
|
|
||||||
console.log(` ✅ BG: ${docs.length} verses`);
|
console.log(` ✅ BG: ${docs.length} verses`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,14 +256,14 @@ async function seedSb(db: ReturnType<MongoClient["db"]>) {
|
|||||||
.filter((d) => fs.statSync(path.join(chaptersDir, d)).isDirectory())
|
.filter((d) => fs.statSync(path.join(chaptersDir, d)).isDirectory())
|
||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
const docs: Record<string, unknown>[] = [];
|
const cantoDocs: Record<string, unknown>[] = [];
|
||||||
|
|
||||||
for (const chDir of chapterDirs) {
|
for (const chDir of chapterDirs) {
|
||||||
const versesDir = path.join(chaptersDir, chDir, "verses");
|
const versesDir = path.join(chaptersDir, chDir, "verses");
|
||||||
const verses = readAllJsonInDir(versesDir) as SbVerse[];
|
const verses = readAllJsonInDir(versesDir) as SbVerse[];
|
||||||
|
|
||||||
for (const v of verses) {
|
for (const v of verses) {
|
||||||
docs.push({
|
cantoDocs.push({
|
||||||
book: "sb",
|
book: "sb",
|
||||||
division: String(v.canto),
|
division: String(v.canto),
|
||||||
chapter: v.chapter,
|
chapter: v.chapter,
|
||||||
@@ -252,11 +281,9 @@ async function seedSb(db: ReturnType<MongoClient["db"]>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (docs.length > 0) {
|
await insertBatched(versesCol, cantoDocs);
|
||||||
await versesCol.insertMany(docs, { ordered: false });
|
totalCount += cantoDocs.length;
|
||||||
}
|
console.log(` Canto ${cantoDir}: ${cantoDocs.length} verses`);
|
||||||
totalCount += docs.length;
|
|
||||||
console.log(` Canto ${cantoDir}: ${docs.length} verses`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(` ✅ SB: ${totalCount} verses`);
|
console.log(` ✅ SB: ${totalCount} verses`);
|
||||||
@@ -305,14 +332,14 @@ async function seedCc(db: ReturnType<MongoClient["db"]>) {
|
|||||||
.filter((d) => fs.statSync(path.join(chaptersDir, d)).isDirectory())
|
.filter((d) => fs.statSync(path.join(chaptersDir, d)).isDirectory())
|
||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
const docs: Record<string, unknown>[] = [];
|
const lilaDocs: Record<string, unknown>[] = [];
|
||||||
|
|
||||||
for (const chDir of chapterDirs) {
|
for (const chDir of chapterDirs) {
|
||||||
const versesDir = path.join(chaptersDir, chDir, "verses");
|
const versesDir = path.join(chaptersDir, chDir, "verses");
|
||||||
const verses = readAllJsonInDir(versesDir) as CcVerse[];
|
const verses = readAllJsonInDir(versesDir) as CcVerse[];
|
||||||
|
|
||||||
for (const v of verses) {
|
for (const v of verses) {
|
||||||
docs.push({
|
lilaDocs.push({
|
||||||
book: "cc",
|
book: "cc",
|
||||||
division: v.lila,
|
division: v.lila,
|
||||||
chapter: v.chapter,
|
chapter: v.chapter,
|
||||||
@@ -330,11 +357,9 @@ async function seedCc(db: ReturnType<MongoClient["db"]>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (docs.length > 0) {
|
await insertBatched(versesCol, lilaDocs);
|
||||||
await versesCol.insertMany(docs, { ordered: false });
|
totalCount += lilaDocs.length;
|
||||||
}
|
console.log(` Līlā ${lilaDir}: ${lilaDocs.length} verses`);
|
||||||
totalCount += docs.length;
|
|
||||||
console.log(` Līlā ${lilaDir}: ${docs.length} verses`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(` ✅ CC: ${totalCount} verses`);
|
console.log(` ✅ CC: ${totalCount} verses`);
|
||||||
@@ -397,12 +422,143 @@ async function seedSb1962(db: ReturnType<MongoClient["db"]>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (docs.length > 0) {
|
await insertBatched(versesCol, docs);
|
||||||
await versesCol.insertMany(docs, { ordered: false });
|
|
||||||
}
|
|
||||||
console.log(` ✅ SB 1962: ${docs.length} verses`);
|
console.log(` ✅ SB 1962: ${docs.length} verses`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Kṛṣṇa Book
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function seedKb(db: ReturnType<MongoClient["db"]>) {
|
||||||
|
const introPath = path.join(LIB_DIR, "books", "kb", "intro.json");
|
||||||
|
if (!fs.existsSync(introPath)) {
|
||||||
|
console.log(" ⏭ KB intro.json not found, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intro = readJson(introPath);
|
||||||
|
|
||||||
|
// Book meta
|
||||||
|
const metaCol = db.collection("book_meta");
|
||||||
|
await metaCol.deleteMany({ book: "kb" });
|
||||||
|
await metaCol.insertOne({ book: "kb", data: intro });
|
||||||
|
console.log(" 📖 KB book meta inserted");
|
||||||
|
|
||||||
|
// Prose chapters collection
|
||||||
|
const proseCol = db.collection("prose_chapters");
|
||||||
|
await proseCol.deleteMany({ book: "kb" });
|
||||||
|
|
||||||
|
const docs: Record<string, unknown>[] = [];
|
||||||
|
|
||||||
|
// Volume 1 intro segments
|
||||||
|
const vol1IntroDir = path.join(LIB_DIR, "books", "kb", "vol-1", "intro");
|
||||||
|
if (fs.existsSync(vol1IntroDir)) {
|
||||||
|
const introFiles = fs
|
||||||
|
.readdirSync(vol1IntroDir)
|
||||||
|
.filter((f) => f.endsWith(".json"))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
const introDocsV1: Record<string, unknown>[] = [];
|
||||||
|
for (const file of introFiles) {
|
||||||
|
const seg = readJson<ProseChapterJson>(path.join(vol1IntroDir, file));
|
||||||
|
introDocsV1.push({
|
||||||
|
book: "kb",
|
||||||
|
volume: seg.volume ?? 1,
|
||||||
|
chapter: seg.chapter ?? null,
|
||||||
|
title: seg.title,
|
||||||
|
type: seg.type || "intro-segment",
|
||||||
|
slug: seg.slug || file.replace(".json", ""),
|
||||||
|
order: seg.order ?? 0,
|
||||||
|
paragraphs: seg.paragraphs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await insertBatched(proseCol, introDocsV1);
|
||||||
|
console.log(` Vol 1 intro segments: ${introFiles.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volume 1 chapters
|
||||||
|
const vol1ChapDir = path.join(LIB_DIR, "books", "kb", "vol-1", "chapters");
|
||||||
|
if (fs.existsSync(vol1ChapDir)) {
|
||||||
|
const chapterFiles = fs
|
||||||
|
.readdirSync(vol1ChapDir)
|
||||||
|
.filter((f) => f.endsWith(".json"))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
const chapDocsV1: Record<string, unknown>[] = [];
|
||||||
|
for (const file of chapterFiles) {
|
||||||
|
const ch = readJson<ProseChapterJson>(path.join(vol1ChapDir, file));
|
||||||
|
chapDocsV1.push({
|
||||||
|
book: "kb",
|
||||||
|
volume: ch.volume ?? 1,
|
||||||
|
chapter: ch.chapter ?? null,
|
||||||
|
title: ch.title,
|
||||||
|
type: ch.type || "chapter",
|
||||||
|
slug: String(ch.chapter ?? file.replace(".json", "")).padStart(2, "0"),
|
||||||
|
order: 100 + (ch.chapter ?? 0),
|
||||||
|
paragraphs: ch.paragraphs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await insertBatched(proseCol, chapDocsV1);
|
||||||
|
console.log(` Vol 1 chapters: ${chapterFiles.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volume 2 intro segments
|
||||||
|
const vol2IntroDir = path.join(LIB_DIR, "books", "kb", "vol-2", "intro");
|
||||||
|
if (fs.existsSync(vol2IntroDir)) {
|
||||||
|
const introFiles = fs
|
||||||
|
.readdirSync(vol2IntroDir)
|
||||||
|
.filter((f) => f.endsWith(".json"))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
const introDocsV2: Record<string, unknown>[] = [];
|
||||||
|
for (const file of introFiles) {
|
||||||
|
const seg = readJson<ProseChapterJson>(path.join(vol2IntroDir, file));
|
||||||
|
introDocsV2.push({
|
||||||
|
book: "kb",
|
||||||
|
volume: seg.volume ?? 2,
|
||||||
|
chapter: seg.chapter ?? null,
|
||||||
|
title: seg.title,
|
||||||
|
type: seg.type || "intro-segment",
|
||||||
|
slug: seg.slug || file.replace(".json", ""),
|
||||||
|
order: seg.order ?? 0,
|
||||||
|
paragraphs: seg.paragraphs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await insertBatched(proseCol, introDocsV2);
|
||||||
|
console.log(` Vol 2 intro segments: ${introFiles.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volume 2 chapters
|
||||||
|
const vol2ChapDir = path.join(LIB_DIR, "books", "kb", "vol-2", "chapters");
|
||||||
|
if (fs.existsSync(vol2ChapDir)) {
|
||||||
|
const chapterFiles = fs
|
||||||
|
.readdirSync(vol2ChapDir)
|
||||||
|
.filter((f) => f.endsWith(".json"))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
const chapDocsV2: Record<string, unknown>[] = [];
|
||||||
|
for (const file of chapterFiles) {
|
||||||
|
const ch = readJson<ProseChapterJson>(path.join(vol2ChapDir, file));
|
||||||
|
chapDocsV2.push({
|
||||||
|
book: "kb",
|
||||||
|
volume: ch.volume ?? 2,
|
||||||
|
chapter: ch.chapter ?? null,
|
||||||
|
title: ch.title,
|
||||||
|
type: ch.type || "chapter",
|
||||||
|
slug: String(ch.chapter ?? file.replace(".json", "")).padStart(2, "0"),
|
||||||
|
order: 100 + (ch.chapter ?? 0),
|
||||||
|
paragraphs: ch.paragraphs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await insertBatched(proseCol, chapDocsV2);
|
||||||
|
console.log(` Vol 2 chapters: ${chapterFiles.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDocs = await proseCol.countDocuments({ book: "kb" });
|
||||||
|
console.log(` ✅ KB: ${totalDocs} prose entries`);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Glossary
|
// Glossary
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -419,16 +575,14 @@ async function seedGlossary(db: ReturnType<MongoClient["db"]>) {
|
|||||||
const col = db.collection("glossary");
|
const col = db.collection("glossary");
|
||||||
await col.deleteMany({});
|
await col.deleteMany({});
|
||||||
|
|
||||||
const docs = data.entries.map((entry) => ({
|
const glossaryDocs = data.entries.map((entry) => ({
|
||||||
term: entry.term,
|
term: entry.term,
|
||||||
definition: entry.definition,
|
definition: entry.definition,
|
||||||
definitionHtml: entry.definition_html,
|
definitionHtml: entry.definition_html,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (docs.length > 0) {
|
await insertBatched(col, glossaryDocs);
|
||||||
await col.insertMany(docs, { ordered: false });
|
console.log(` ✅ Glossary: ${glossaryDocs.length} entries`);
|
||||||
}
|
|
||||||
console.log(` ✅ Glossary: ${docs.length} entries`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -453,6 +607,12 @@ async function createIndexes(db: ReturnType<MongoClient["db"]>) {
|
|||||||
const glossaryCol = db.collection("glossary");
|
const glossaryCol = db.collection("glossary");
|
||||||
await glossaryCol.createIndex({ term: 1 }, { unique: true });
|
await glossaryCol.createIndex({ term: 1 }, { unique: true });
|
||||||
|
|
||||||
|
const proseCol = db.collection("prose_chapters");
|
||||||
|
await proseCol.createIndex({ book: 1 });
|
||||||
|
await proseCol.createIndex({ book: 1, volume: 1 });
|
||||||
|
await proseCol.createIndex({ book: 1, volume: 1, slug: 1 }, { unique: true });
|
||||||
|
await proseCol.createIndex({ book: 1, volume: 1, type: 1 });
|
||||||
|
|
||||||
console.log(" ✅ Indexes created");
|
console.log(" ✅ Indexes created");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,11 +647,15 @@ async function main() {
|
|||||||
await seedSb1962(db);
|
await seedSb1962(db);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
console.log("[5/6] Glossary");
|
console.log("[5/7] Kṛṣṇa Book");
|
||||||
|
await seedKb(db);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
console.log("[6/7] Glossary");
|
||||||
await seedGlossary(db);
|
await seedGlossary(db);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
console.log("[6/6] Indexes");
|
console.log("[7/7] Indexes");
|
||||||
await createIndexes(db);
|
await createIndexes(db);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import dotenv from "dotenv";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { MongoClient } from "mongodb";
|
||||||
|
|
||||||
|
dotenv.config({ path: ".env.local" });
|
||||||
|
dotenv.config({ path: ".env" });
|
||||||
|
|
||||||
|
const DATABASE_URL = process.env.DATABASE_URL;
|
||||||
|
if (!DATABASE_URL) {
|
||||||
|
console.error("❌ DATABASE_URL is not set");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LIB_DIR = path.join(process.cwd(), "lib", "jsons");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const client = new MongoClient(DATABASE_URL!);
|
||||||
|
await client.connect();
|
||||||
|
console.log("📡 Connected to MongoDB");
|
||||||
|
|
||||||
|
const db = client.db();
|
||||||
|
const col = db.collection("prose_chapters");
|
||||||
|
|
||||||
|
// Delete existing Vol 2 data
|
||||||
|
const deleted = await col.deleteMany({ book: "kb", volume: 2 });
|
||||||
|
console.log(`🗑 Cleared ${deleted.deletedCount} existing Vol 2 docs`);
|
||||||
|
|
||||||
|
const chapDir = path.join(LIB_DIR, "books", "kb", "vol-2", "chapters");
|
||||||
|
if (!fs.existsSync(chapDir)) {
|
||||||
|
console.error(`ERROR: ${chapDir} not found!`);
|
||||||
|
await client.close();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(chapDir)
|
||||||
|
.filter((f) => f.endsWith(".json"))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
console.log(`\n📝 Seeding ${files.length} Vol 2 chapters...\n`);
|
||||||
|
|
||||||
|
let success = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const data = JSON.parse(fs.readFileSync(path.join(chapDir, file), "utf-8"));
|
||||||
|
const slug = String(data.chapter).padStart(2, "0");
|
||||||
|
const filter = { book: "kb" as const, volume: 2, slug };
|
||||||
|
|
||||||
|
// Sanitize title: encode non-ASCII chars as HTML entities to avoid
|
||||||
|
// BSON wire-protocol corruption on the initial insertOne
|
||||||
|
const safeTitle = (data.title as string).replace(
|
||||||
|
/[^\x20-\x7E]/g,
|
||||||
|
(ch: string) => `&#${ch.codePointAt(0)};`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Insert empty document with ASCII-safe title (small, won't corrupt)
|
||||||
|
await col.insertOne({
|
||||||
|
book: "kb",
|
||||||
|
volume: 2,
|
||||||
|
chapter: data.chapter ?? null,
|
||||||
|
title: safeTitle,
|
||||||
|
type: "chapter",
|
||||||
|
slug,
|
||||||
|
order: 100 + (data.chapter ?? 0),
|
||||||
|
paragraphs: [] as string[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 1b: Restore the real Unicode title via $set update
|
||||||
|
// (small update, less likely to corrupt than the initial insert)
|
||||||
|
try {
|
||||||
|
await col.updateOne(filter, { $set: { title: data.title } });
|
||||||
|
} catch {
|
||||||
|
// If title update fails, keep the HTML-entity version — it's readable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Push paragraphs one at a time via $push
|
||||||
|
// Each update is tiny so it avoids BSON wire corruption.
|
||||||
|
// If a paragraph fails (BSON encoding error), retry with ASCII-safe version.
|
||||||
|
for (let i = 0; i < data.paragraphs.length; i++) {
|
||||||
|
const para: string = data.paragraphs[i];
|
||||||
|
try {
|
||||||
|
await col.updateOne(filter, { $push: { paragraphs: para } });
|
||||||
|
} catch {
|
||||||
|
// Fallback: strip non-ASCII characters that may cause wire corruption
|
||||||
|
const safePara = para.replace(
|
||||||
|
/[^\x20-\x7E\n\r\t<>/='"&;.\-!?:,()[\]{}#@*+%$^~`|\\]/g,
|
||||||
|
function (ch) {
|
||||||
|
// Keep common IAST diacritical characters by encoding as HTML entities
|
||||||
|
const cp = ch.codePointAt(0);
|
||||||
|
if (cp !== undefined) return `&#${cp};`;
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await col.updateOne(filter, { $push: { paragraphs: safePara } });
|
||||||
|
} catch {
|
||||||
|
// Last resort: push a placeholder
|
||||||
|
await col.updateOne(filter, {
|
||||||
|
$push: {
|
||||||
|
paragraphs: `[Paragraph ${i + 1} could not be stored due to encoding issues]`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.warn(
|
||||||
|
` ⚠ Ch ${data.chapter} para ${i + 1}: encoding fallback`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Verify round-trip
|
||||||
|
const readBack = await col.findOne(filter);
|
||||||
|
if (
|
||||||
|
readBack &&
|
||||||
|
Array.isArray(readBack.paragraphs) &&
|
||||||
|
readBack.paragraphs.length === data.paragraphs.length
|
||||||
|
) {
|
||||||
|
success++;
|
||||||
|
console.log(
|
||||||
|
` ✓ Ch ${data.chapter}: ${data.paragraphs.length} paragraphs — ${data.title}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
failed++;
|
||||||
|
console.log(
|
||||||
|
` ✗ Ch ${data.chapter}: verification failed (expected ${data.paragraphs.length}, got ${readBack?.paragraphs?.length ?? "null"})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
failed++;
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
console.log(` ✗ Ch ${data.chapter}: ${msg.substring(0, 100)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final count
|
||||||
|
const total = await col.countDocuments({ book: "kb" });
|
||||||
|
console.log(`\n--- Results ---`);
|
||||||
|
console.log(`Success: ${success} | Failed: ${failed}`);
|
||||||
|
console.log(`Total KB prose docs in DB: ${total}`);
|
||||||
|
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error("❌ FATAL:", e.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+6
-6
@@ -15,12 +15,12 @@
|
|||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"**/*.mts"
|
"**/*.mts",
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "scripts"],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2640,7 +2640,7 @@ internal-slot@^1.1.0:
|
|||||||
hasown "^2.0.2"
|
hasown "^2.0.2"
|
||||||
side-channel "^1.1.0"
|
side-channel "^1.1.0"
|
||||||
|
|
||||||
ip-address@10.1.0:
|
ip-address@^10.0.1, ip-address@10.1.0:
|
||||||
version "10.1.0"
|
version "10.1.0"
|
||||||
resolved "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz"
|
resolved "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz"
|
||||||
integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==
|
integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==
|
||||||
@@ -4185,6 +4185,19 @@ sisteransi@^1.0.5:
|
|||||||
resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz"
|
resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz"
|
||||||
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
|
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
|
||||||
|
|
||||||
|
smart-buffer@^4.2.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz"
|
||||||
|
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
|
||||||
|
|
||||||
|
socks@^2.8.6:
|
||||||
|
version "2.8.7"
|
||||||
|
resolved "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz"
|
||||||
|
integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==
|
||||||
|
dependencies:
|
||||||
|
ip-address "^10.0.1"
|
||||||
|
smart-buffer "^4.2.0"
|
||||||
|
|
||||||
source-map-js@^1.0.2, source-map-js@^1.2.1:
|
source-map-js@^1.0.2, source-map-js@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
|
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user