// --------------------------------------------------------------------------- // 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

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

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 }; } /** * Resolve a post's source into structured `{ title, detail }` for * multi-line share/copy payloads. Mirrors the formatCopyText pattern * from selection-toolbar/shared.tsx so Send + Copy produce the same * citation block. * * Two routes to the structured shape: * * 1. Book URLs → `referenceFromUrl` parses the path. * 2. Anything else (letters, essays, transcriptions, magazines): * `source_title` was composed by compose-post.tsx as * "Title — Detail" using ` — ` as the join. Split on the first * occurrence to recover the structured fields. * * Falls back to title-only with no detail when no separator is present. */ export function structuredReferenceForPost( sourceUrl: string, sourceTitle: string | null | undefined, ): { title: string; detail: string | null } { const r = referenceFromUrl(sourceUrl); if (r) return r; const stored = sourceTitle?.trim() || "Source"; const idx = stored.indexOf(" — "); if (idx === -1) return { title: stored, detail: null }; return { title: stored.slice(0, idx).trim(), detail: stored.slice(idx + 3).trim() || 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; }