Files
spsays-training/lib/source-reference.ts
T

387 lines
12 KiB
TypeScript

// ---------------------------------------------------------------------------
// Source-reference helpers (browser-only)
//
// These functions access `window` and `document` and therefore can only run in
// the browser. They are intentionally NOT marked "use client" because they are
// plain utility functions, not React components or hooks. Any React component
// that calls them must itself be a client component.
// ---------------------------------------------------------------------------
import { BOOK_NAMES, CC_LILAS } from "@/lib/constants";
import { buildHighlightUrl } from "@/lib/text-highlight";
/**
* Parse the current page URL into a human-readable source reference.
* Returns { title, detail, url } or null if on a non-content page.
*/
export function getSourceReference(): {
title: string;
detail: string | null;
url: string;
} | null {
const url = window.location.href;
const path = window.location.pathname;
const parts = path.split("/").filter(Boolean);
// /books/bg/1/1 → Bhagavad-gītā As It Is, Chapter 1, Verse 1
if (parts[0] === "books" && parts.length >= 2) {
const code = parts[1];
const bookName = BOOK_NAMES[code] || code.toUpperCase();
// Verse-based: /books/bg/[chapter]/[verse]
if (code === "bg" && parts.length >= 4) {
return {
title: bookName,
detail: `Chapter ${parts[2]}, Verse ${parts[3]}`,
url,
};
}
// SB: /books/sb/[canto]/[chapter]/[verse]
if (code === "sb" && parts.length >= 5) {
return {
title: bookName,
detail: `Canto ${parts[2]}, Chapter ${parts[3]}, Verse ${parts[4]}`,
url,
};
}
// CC: /books/cc/[lila]/[chapter]/[verse]
if (code === "cc" && parts.length >= 5) {
const lila = CC_LILAS[parts[2]] || parts[2];
return {
title: bookName,
detail: `${lila}, Chapter ${parts[3]}, Verse ${parts[4]}`,
url,
};
}
// CC lila index: /books/cc/[lila]
if (code === "cc" && parts.length === 3) {
const lila = CC_LILAS[parts[2]] || parts[2];
return { title: bookName, detail: lila, url };
}
// SB1962: /books/sb1962/[chapter]/[verse]
if (code === "sb1962" && parts.length >= 4) {
return {
title: bookName,
detail: `Chapter ${parts[2]}, Verse ${parts[3]}`,
url,
};
}
// ISO/NOI: /books/iso/[mantra]
if ((code === "iso" || code === "noi") && parts.length >= 3) {
const num = parts[2];
const label = code === "iso" ? "Mantra" : "Text";
return { title: bookName, detail: `${label} ${num}`, url };
}
// BS/MM/NBS: /books/{code}/[chapter]/[verse]
if (
(code === "bs" || code === "mm" || code === "nbs") &&
parts.length >= 4
) {
return {
title: bookName,
detail: `Chapter ${parts[2]}, Verse ${parts[3]}`,
url,
};
}
// KB: /books/kb/[volume]/[slug]
if (code === "kb" && parts.length >= 4) {
return {
title: bookName,
detail: `Volume ${parts[2]}, Chapter ${parts[3]}`,
url,
};
}
// Generic prose: /books/{code}/[chapter]
if (parts.length >= 3) {
const chSlug = parts[2];
const chNum = parseInt(chSlug, 10);
const detail = isNaN(chNum)
? chSlug.replace(/-/g, " ")
: `Chapter ${chNum}`;
return { title: bookName, detail, url };
}
// Book index page
return { title: bookName, detail: null, url };
}
// /letters/[slug]
if (
parts[0] === "letters" &&
parts.length === 2 &&
parts[1] !== "year" &&
parts[1] !== "recipient" &&
parts[1] !== "location"
) {
// h1 contains "Letter to Mukunda" — use it as the title
const h1 = document.querySelector("h1");
const title = h1?.textContent?.trim() || "Letter";
// The <p> sibling after h1 contains "15 January, 1968 · Calcutta"
const metaP = document.querySelector(
"header p.text-muted-foreground, header p[class*='muted']",
);
const meta = metaP?.textContent?.trim() || null;
return { title, detail: meta, url };
}
// /essays/[slug]
if (parts[0] === "essays" && parts.length === 2) {
const h1 = document.querySelector("h1");
const title = h1?.textContent?.trim() || parts[1].replace(/-/g, " ");
return { title, detail: "Essay by Śrīla Prabhupāda", url };
}
// /journals/[slug]/[chapter]
if (parts[0] === "journals" && parts.length >= 3) {
const backLink = document.querySelector("header a[href^='/journals/']");
const parentTitle =
backLink?.textContent?.replace(/^←\s*/, "").trim() || "Journal";
const h1 = document.querySelector("h1");
const chapterTitle = h1?.textContent?.trim() || null;
return { title: parentTitle, detail: chapterTitle, url };
}
// /magazines/[slug]/[issue]
if (parts[0] === "magazines" && parts.length >= 3) {
const backLink = document.querySelector("header a[href^='/magazines/']");
const parentTitle =
backLink?.textContent?.replace(/^←\s*/, "").trim() || "Magazine";
const h1 = document.querySelector("h1");
const issueTitle = h1?.textContent?.trim() || null;
return { title: parentTitle, detail: issueTitle, url };
}
// /other-documents/[slug]/[chapter]
if (parts[0] === "other-documents" && parts.length >= 3) {
const backLink = document.querySelector(
"header a[href^='/other-documents/']",
);
const parentTitle =
backLink?.textContent?.replace(/^←\s*/, "").trim() || "Document";
const h1 = document.querySelector("h1");
const chapterTitle = h1?.textContent?.trim() || null;
return { title: parentTitle, detail: chapterTitle, url };
}
// /transcriptions/[slug]
if (
parts[0] === "transcriptions" &&
parts.length === 2 &&
parts[1] !== "year" &&
parts[1] !== "place" &&
parts[1] !== "type"
) {
const h1 = document.querySelector("h1");
const title =
"Transcripts: " + (h1?.textContent?.trim() || "Transcription");
// The metadata line contains badges + date + place + reference + duration
// Grab the text from the header's muted-foreground elements after h1
const metaContainer = document.querySelector(
"header .flex.items-center.gap-3",
);
if (metaContainer) {
// Collect text from spans (skip badges which are in their own elements)
const spans = metaContainer.querySelectorAll("span");
const metaParts: string[] = [];
spans.forEach((span) => {
const text = span.textContent?.trim();
if (
text &&
text !== "Lecture" &&
text !== "Conversation" &&
text.length > 1 &&
!text.includes("min") &&
!span.closest(".text-primary")
) {
metaParts.push(text);
}
});
const detail = metaParts.join(" · ") || null;
return { title, detail, url };
}
// Fallback: try the <p> after h1
const metaP = document.querySelector(
"header p.text-muted-foreground, header p[class*='muted']",
);
const meta = metaP?.textContent?.trim() || null;
return { title, detail: meta, url };
}
// /glossary/[letter]?term=...
if (parts[0] === "glossary") {
const h1 = document.querySelector("h1");
const termFromH1 = h1?.textContent?.replace(/^Glossary:\s*/, "").trim();
// Try to get the term from the URL ?term= param
const urlParams = new URLSearchParams(window.location.search);
const termFromUrl = urlParams.get("term");
const term = termFromUrl || termFromH1 || null;
if (term) {
return { title: term, detail: "Glossary — Prabhupāda Says", url };
}
// Fallback: letter-level
const letter = parts[1]?.toUpperCase();
if (letter) {
return { title: `Glossary — ${letter}`, detail: null, url };
}
return { title: "Glossary", detail: null, url };
}
// Fallback: try the h1
const h1 = document.querySelector("h1");
if (h1) {
return {
title: h1.textContent?.trim() || "Prabhupāda Says",
detail: null,
url,
};
}
return null;
}
/**
* URL-only variant of getSourceReference — pure function, safe on server.
*
* For `/books/...` paths we can derive a clean citation (e.g.
* "Bhagavad-gītā As It Is — Chapter 1, Verse 1") from the path alone.
* Mirrors the logic in `lib/og.ts`'s `ogForVerse` so post cards display
* the same reference the OG image carries.
*
* For non-book paths the title lives in the page DOM (h1) which we don't
* have at card-render time, so we return null — callers should fall back
* to whatever `source_title` was saved at compose time.
*/
export function referenceFromUrl(
sourceUrl: string,
): { title: string; detail: string | null } | null {
if (!sourceUrl) return null;
let path: string;
try {
if (sourceUrl.startsWith("/")) {
path = sourceUrl.split("?")[0];
} else {
const u = new URL(sourceUrl);
path = u.pathname;
}
} catch {
return null;
}
const parts = path.split("/").filter(Boolean);
if (parts[0] !== "books" || parts.length < 2) return null;
const code = parts[1];
const bookName = BOOK_NAMES[code] || code.toUpperCase();
// Verse-based: /books/bg/[chapter]/[verse]
if (code === "bg" && parts.length >= 4) {
return {
title: bookName,
detail: `Chapter ${parts[2]}, Verse ${parts[3]}`,
};
}
// SB: /books/sb/[canto]/[chapter]/[verse]
if (code === "sb" && parts.length >= 5) {
return {
title: bookName,
detail: `Canto ${parts[2]}, Chapter ${parts[3]}, Verse ${parts[4]}`,
};
}
// CC: /books/cc/[lila]/[chapter]/[verse]
if (code === "cc" && parts.length >= 5) {
const lila = CC_LILAS[parts[2]] || parts[2];
return {
title: bookName,
detail: `${lila}, Chapter ${parts[3]}, Verse ${parts[4]}`,
};
}
// CC lila index: /books/cc/[lila]
if (code === "cc" && parts.length === 3) {
const lila = CC_LILAS[parts[2]] || parts[2];
return { title: bookName, detail: lila };
}
// SB1962: /books/sb1962/[chapter]/[verse]
if (code === "sb1962" && parts.length >= 4) {
return {
title: bookName,
detail: `Chapter ${parts[2]}, Verse ${parts[3]}`,
};
}
// ISO / NOI: /books/{code}/[mantra]
if ((code === "iso" || code === "noi") && parts.length >= 3) {
const num = parts[2];
const label = code === "iso" ? "Mantra" : "Text";
return { title: bookName, detail: `${label} ${num}` };
}
// BS/MM/NBS verse: /books/{code}/[chapter]/[verse]
if (
(code === "bs" || code === "mm" || code === "nbs") &&
parts.length >= 4
) {
return {
title: bookName,
detail: `Chapter ${parts[2]}, Verse ${parts[3]}`,
};
}
// KB: /books/kb/[volume]/[slug]
if (code === "kb" && parts.length >= 4) {
return {
title: bookName,
detail: `Volume ${parts[2]}, Chapter ${parts[3]}`,
};
}
// Generic prose: /books/{code}/[chapter]
if (parts.length >= 3) {
const chSlug = parts[2];
const chNum = parseInt(chSlug, 10);
const detail = isNaN(chNum)
? chSlug.replace(/-/g, " ")
: `Chapter ${chNum}`;
return { title: bookName, detail };
}
// Book index
return { title: bookName, detail: null };
}
/**
* Render a one-line citation string for a post/bookmark card.
*
* For book URLs we always prefer the URL-derived reference (canonical
* format, no stale data). For non-book content (letters, essays, etc.)
* the saved `source_title` is the source of truth.
*/
export function displaySourceLabel(
sourceUrl: string,
storedTitle?: string | null,
): string {
const ref = referenceFromUrl(sourceUrl);
if (ref) {
return ref.detail ? `${ref.title} — ${ref.detail}` : ref.title;
}
return storedTitle?.trim() || "Source";
}
/**
* Format selected text with source attribution appended.
*/
export function formatCopyText(selectedText: string): string {
const ref = getSourceReference();
if (!ref) return selectedText;
const highlightUrl = buildHighlightUrl(selectedText);
let source = `\n\nSource:\n${ref.title}`;
if (ref.detail) source += `\n${ref.detail}`;
source += `\n${highlightUrl}`;
return selectedText + source;
}