/* Tripiṭaka Widget (Fallback + Enhanced) */
#trpk-widget.trpk{
–bg:#0b1020; –panel:rgba(255,255,255,.06); –panel2:rgba(255,255,255,.10);
–text:rgba(255,255,255,.92); –muted:rgba(255,255,255,.70); –border:rgba(255,255,255,.14);
–shadow:0 18px 60px rgba(0,0,0,.45); –focus:rgba(125,211,252,.18); –focusB:rgba(125,211,252,.70);
–danger:rgba(244,63,94,.55); –dangerBg:rgba(244,63,94,.10);
color:var(–text); background:var(–bg); border:1px solid var(–border); border-radius:16px; overflow:hidden;
box-shadow:var(–shadow); max-width:1100px; margin:16px auto;
font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
}
#trpk-widget.trpk[data-theme=”light”]{
–bg:#f7f7fb; –panel:rgba(0,0,0,.04); –panel2:rgba(0,0,0,.06);
–text:rgba(0,0,0,.88); –muted:rgba(0,0,0,.62); –border:rgba(0,0,0,.12);
–shadow:0 18px 60px rgba(0,0,0,.12); –focus:rgba(3,105,161,.12); –focusB:rgba(3,105,161,.55);
}
#trpk-widget a{color:inherit}
#trpk-widget .trpk-top{display:flex;gap:12px;align-items:center;justify-content:space-between;padding:12px;border-bottom:1px solid var(–border);background:linear-gradient(180deg,var(–panel2),transparent)}
#trpk-widget .trpk-brand{display:flex;gap:10px;align-items:center;min-width:240px}
#trpk-widget .trpk-logo{font-size:20px}
#trpk-widget .trpk-title{font-weight:900;font-size:14px}
#trpk-widget .trpk-sub{font-size:12px;color:var(–muted);margin-top:2px;line-height:1.25}
#trpk-widget .trpk-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
#trpk-widget .trpk-btn,#trpk-widget .trpk-input,#trpk-widget .trpk-select{
border:1px solid var(–border); background:var(–panel); color:var(–text);
border-radius:10px; padding:9px 10px; font:inherit;
}
#trpk-widget .trpk-btn{cursor:pointer}
#trpk-widget .trpk-btn-sm{padding:8px 9px;font-size:12px}
#trpk-widget .trpk-input{width:min(260px,44vw);outline:none}
#trpk-widget .trpk-input:focus{border-color:var(–focusB);box-shadow:0 0 0 3px var(–focus)}
#trpk-widget .trpk-fallback{display:grid;grid-template-columns:330px 1fr;min-height:520px}
#trpk-widget .trpk-fb-nav{border-right:1px solid var(–border);padding:10px;background:linear-gradient(180deg,var(–panel),transparent)}
#trpk-widget .trpk-fb-hd{font-weight:900;font-size:13px;margin:4px 0 10px}
#trpk-widget .trpk-fb-hd2{font-weight:900;font-size:12px;color:var(–muted);margin:14px 0 8px}
#trpk-widget .trpk-link{display:block;padding:10px;border:1px solid var(–border);border-radius:12px;background:var(–panel);text-decoration:none;margin-bottom:8px}
#trpk-widget .trpk-link:hover{border-color:rgba(125,211,252,.55)}
#trpk-widget .trpk-fb-note{font-size:12px;color:var(–muted);line-height:1.35;margin-top:10px}
#trpk-widget .trpk-fb-main{padding:10px}
#trpk-widget .trpk-iframe{width:100%;height:640px;border:1px solid var(–border);border-radius:14px;background:#fff}
#trpk-widget .trpk-app{display:none}
#trpk-widget .trpk-body{display:grid;grid-template-columns:360px 1fr;min-height:520px}
#trpk-widget .trpk-nav{border-right:1px solid var(–border);background:linear-gradient(180deg,var(–panel),transparent)}
#trpk-widget .trpk-navhd{display:flex;gap:8px;align-items:center;justify-content:space-between;padding:10px;border-bottom:1px solid var(–border)}
#trpk-widget .trpk-navc{padding:10px;max-height:560px;overflow:auto}
#trpk-widget .trpk-leaf{width:100%;text-align:left;border:1px solid var(–border);background:var(–panel);border-radius:12px;padding:10px;margin-top:8px}
#trpk-widget .trpk-leaf .t{display:block;font-weight:900;font-size:13px}
#trpk-widget .trpk-leaf .m{display:block;color:var(–muted);font-size:12px;margin-top:4px;line-height:1.25}
#trpk-widget .trpk-readerhd{display:flex;gap:10px;align-items:flex-start;justify-content:space-between;padding:12px;border-bottom:1px solid var(–border);background:linear-gradient(180deg,var(–panel2),transparent)}
#trpk-widget .trpk-crumb{font-size:12px;color:var(–muted)}
#trpk-widget .trpk-rt{font-size:14px;font-weight:900;margin-top:6px}
#trpk-widget .trpk-tools{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
#trpk-widget .trpk-content{padding:14px 12px 20px;max-height:640px;overflow:auto}
#trpk-widget .trpk-verse{display:grid;grid-template-columns:56px 1fr;gap:12px;align-items:start;padding:12px;border:1px solid var(–border);border-radius:14px;background:var(–panel);margin-bottom:12px}
#trpk-widget .trpk-vn{width:46px;height:34px;border-radius:12px;border:1px solid var(–border);background:transparent;font-weight:900;color:var(–text);cursor:pointer}
#trpk-widget .trpk-vt{font-size:14px;line-height:1.55;white-space:pre-wrap}
#trpk-widget .trpk-err{display:none;margin:12px;padding:12px;border:1px solid var(–danger);border-radius:12px;background:var(–dangerBg)}
#trpk-widget .trpk-toast{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);background:rgba(0,0,0,.75);color:#fff;padding:10px 12px;border-radius:999px;font-size:12px;opacity:0;pointer-events:none;transition:opacity 160ms ease;z-index:999999}
#trpk-widget .trpk-toast.on{opacity:1}
#trpk-widget .trpk-modal{position:fixed;inset:0;display:none;z-index:999998}
#trpk-widget .trpk-modal.on{display:block}
#trpk-widget .trpk-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.55)}
#trpk-widget .trpk-card{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:min(680px,92vw);max-height:min(600px,86vh);border:1px solid var(–border);border-radius:16px;background:var(–bg);box-shadow:var(–shadow);overflow:hidden}
#trpk-widget .trpk-cardhd{display:flex;align-items:center;justify-content:space-between;padding:12px;border-bottom:1px solid var(–border)}
#trpk-widget .trpk-cardbd{padding:12px;max-height:520px;overflow:auto}
@media (max-width:900px){
#trpk-widget .trpk-fallback{grid-template-columns:1fr}
#trpk-widget .trpk-fb-nav{border-right:none;border-bottom:1px solid var(–border)}
#trpk-widget .trpk-iframe{height:560px}
#trpk-widget .trpk-body{grid-template-columns:1fr}
#trpk-widget .trpk-nav{border-right:none;border-bottom:1px solid var(–border)}
#trpk-widget .trpk-navc{max-height:260px}
}
(() => {
const root = document.getElementById(“trpk-widget”);
if (!root) return;
// Theme toggle (works even if API part fails)
const setTheme = (t) => root.setAttribute(“data-theme”, t);
const themeKey = “trpk_theme_v1”;
let theme = localStorage.getItem(themeKey) || root.getAttribute(“data-theme”) || “dark”;
setTheme(theme);
// UI refs
const fallback = document.getElementById(“trpk-fallback”);
const app = document.getElementById(“trpk-app”);
const navList = document.getElementById(“trpk-navList”);
const search = document.getElementById(“trpk-search”);
const crumb = document.getElementById(“trpk-crumb”);
const rt = document.getElementById(“trpk-rt”);
const chapter = document.getElementById(“trpk-chapter”);
const prevBtn = document.getElementById(“trpk-prev”);
const nextBtn = document.getElementById(“trpk-next”);
const copyRefBtn = document.getElementById(“trpk-copyref”);
const content = document.getElementById(“trpk-content”);
const err = document.getElementById(“trpk-err”);
const errmsg = document.getElementById(“trpk-errmsg”);
const toastEl = document.getElementById(“trpk-toast”);
const modal = document.getElementById(“trpk-modal”);
const modalBody = document.getElementById(“trpk-modalBody”);
const API = root.dataset.wsapi || “
https://en.wikisource.org/w/api.php” ;;
const K = { cont:”trpk_cont_v1″, bms:”trpk_bms_v1″, dnCache:”trpk_dn_cache_v1″ };
const jparse=(s,f)=>{ try{ const v=JSON.parse(s); return v==null?f:v; }catch{ return f; } };
const esc=(s)=>String(s).replace(/&/g,”&”).replace(//g,”>”).replace(/”/g,”"”).replace(/’/g,”'”);
const collapse=(s)=>String(s||””).replace(/\s+/g,” “).trim();
const clamp=(n,a,b)=>Math.max(a,Math.min(b,n));
const mem = new Map();
let state = jparse(localStorage.getItem(K.cont), { view:”library” });
let bookmarks = jparse(localStorage.getItem(K.bms), []);
const LIBRARY = [
{ type:”collection”, id:”dn”, title:”Dīgha Nikāya (Long Discourses)”, note:”List from Portal:Tipitaka/Digha_Nikaya” },
{ type:”book”, id:”sbe10″, title:”Sutta Nipāta (SBE Vol. X)”, note:”Wikisource PD edition”, page:”Sacred_Books_of_the_East/Volume_10″ },
{ type:”book”, id:”sbe11″, title:”Buddhist Suttas (SBE Vol. XI)”, note:”Wikisource PD edition”, page:”Sacred_Books_of_the_East/Volume_11″ },
{ type:”book”, id:”sbe13″, title:”Vinaya Texts (SBE Vol. XIII)”, note:”Wikisource PD edition”, page:”Sacred_Books_of_the_East/Volume_13″ },
{ type:”book”, id:”sbe17″, title:”Vinaya Texts (SBE Vol. XVII)”, note:”Wikisource PD edition”, page:”Sacred_Books_of_the_East/Volume_17″ },
{ type:”book”, id:”sbe20″, title:”Vinaya Texts (SBE Vol. XX)”, note:”Wikisource PD edition”, page:”Sacred_Books_of_the_East/Volume_20″ },
{ type:”book”, id:”portal”, title:”Tipiṭaka Portal (Index)”, note:”Wikisource hub page”, page:”Portal:Tipitaka” }
];
function toast(msg){
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add(“on”);
setTimeout(()=>toastEl.classList.remove(“on”), 1700);
}
function showErr(e){
if (err) err.style.display=”block”;
if (errmsg) errmsg.textContent = (e && e.message) ? e.message : String(e);
}
function clearErr(){
if (err) err.style.display=”none”;
if (errmsg) errmsg.textContent = “”;
}
function saveState(){ localStorage.setItem(K.cont, JSON.stringify(state)); }
function saveBms(){ localStorage.setItem(K.bms, JSON.stringify(bookmarks)); }
async function copyText(text){
try{ if(navigator.clipboard?.writeText){ await navigator.clipboard.writeText(text); return; } }catch{}
const ta=document.createElement(“textarea”);
ta.value=text; ta.style.position=”fixed”; ta.style.opacity=”0″;
document.body.appendChild(ta); ta.select(); document.execCommand(“copy”);
document.body.removeChild(ta);
}
async function apiParseText(page, section){
const key=`t:${page}:${String(section||”0″)}`;
if(mem.has(key)) return mem.get(key);
const url = API + “?action=parse&format=json&origin=*&page=” + encodeURIComponent(page) +
“&prop=text§ion=” + encodeURIComponent(String(section||”0″));
const r = await fetch(url, {cache:”no-cache”});
if(!r.ok) throw new Error(“Wikisource API error (“+r.status+”)”);
const d = await r.json();
const html = (d?.parse?.text?.[“*”]) || “”;
mem.set(key, html);
return html;
}
async function apiParseSections(page){
const key=`s:${page}`;
if(mem.has(key)) return mem.get(key);
const url = API + “?action=parse&format=json&origin=*&page=” + encodeURIComponent(page) + “&prop=sections”;
const r = await fetch(url, {cache:”no-cache”});
if(!r.ok) throw new Error(“Wikisource API error (“+r.status+”)”);
const d = await r.json();
const raw = d?.parse?.sections || [];
const secs = [{index:”0″, line:”(Full text)”}].concat(raw.map(s=>({index:String(s.index), line:String(s.line)})));
mem.set(key, secs);
return secs;
}
function htmlToBlocks(html){
const tmp=document.createElement(“div”);
tmp.innerHTML = html || “”;
tmp.querySelectorAll(“sup.reference, span.mw-editsection, .mw-editsection, .reference”).forEach(n=>n.remove());
const blocks=[];
tmp.querySelectorAll(“p, li”).forEach(el=>{
const t = collapse(el.textContent || “”);
if (t.length>=3) blocks.push(t);
});
if(!blocks.length){
const t = collapse(tmp.textContent || “”);
if(t) blocks.push(t);
}
return blocks;
}
function setChapterOptions(secs, cur){
if(!chapter) return;
if(!secs || secs.length{
const idx=String(s.index), label=s.line || (“Section “+idx);
return `${esc(label)}`;
}).join(“”);
}
function currentRef(){
if(state.view!==”page”) return “Wikisource”;
const sec = chapter?.selectedOptions?.[0]?.textContent || “”;
const base = (state.label||state.title||”Wikisource”).replace(/_/g,” “);
return (sec && sec!==”(Full text)”) ? (base+” — “+sec) : base;
}
function renderBlocks(blocks, refBase){
if(!content) return;
if(!blocks?.length){
content.innerHTML = `
No readable text found.
`;
return;
}
content.innerHTML = blocks.map((txt,i)=>{
const n=String(i+1), ref=`${refBase}:${i+1}`;
return `
`;
}).join(“”);
}
function filterNav(){
if(!search || !navList) return;
const q=(search.value||””).trim().toLowerCase();
const btns=[…navList.querySelectorAll(“button.trpk-leaf”)];
if(!q){ btns.forEach(b=>b.style.display=”block”); return; }
btns.forEach(b=>{ b.style.display=(b.textContent||””).toLowerCase().includes(q)?”block”:”none”; });
}
async function loadDnList(){
const cached=jparse(localStorage.getItem(K.dnCache),null);
if(cached?.items?.length) return cached.items;
const html = await apiParseText(“Portal:Tipitaka/Digha_Nikaya”,”0″);
const tmp=document.createElement(“div”); tmp.innerHTML=html;
const anchors=[…tmp.querySelectorAll(“a”)];
const titles=[];
for(const a of anchors){
const t=a.getAttribute(“title”)||””, href=a.getAttribute(“href”)||””;
if(!href.startsWith(“/wiki/”)) continue;
if(t.includes(“Portal:”)||t.includes(“Index:”)) continue;
if(!t.toLowerCase().includes(“sutta”)) continue;
titles.push(t);
}
const seen=new Set(), uniq=[];
for(const t of titles){ if(seen.has(t)) continue; seen.add(t); uniq.push(t); }
const items=uniq.map((t,i)=>({title:t.replace(/ /g,”_”), label:`DN ${i+1} — ${t}`}));
localStorage.setItem(K.dnCache, JSON.stringify({at:new Date().toISOString(), items}));
return items;
}
async function openLibrary(){
clearErr();
state={view:”library”}; saveState();
if(crumb) crumb.textContent=”Wikisource”;
if(rt) rt.textContent=”Library”;
setChapterOptions(null,”0″);
if(content) content.innerHTML = `
Select a text from the left.
`;
if(navList){
navList.innerHTML = LIBRARY.map(x=>{
return `
${esc(x.title)} ${esc(x.note||””)} `;
}).join(“”);
}
filterNav();
}
async function openDn(){
clearErr();
state={view:”collection”, id:”dn”}; saveState();
if(crumb) crumb.textContent=”Collection”;
if(rt) rt.textContent=”Dīgha Nikāya (DN)”;
setChapterOptions(null,”0″);
if(content) content.innerHTML = `
Pick a sutta from the left.
`;
if(navList) navList.innerHTML = `
Loading DN list…
`;
const items = await loadDnList();
if(navList){
navList.innerHTML = items.map(it=>{
return `
${esc(it.label)} Wikisource `;
}).join(“”);
}
filterNav();
}
async function openPage(title,label,section){
clearErr();
if(content) content.textContent=”Loading…”;
state={view:”page”, title, label:label||title, section:String(section||”0″)}; saveState();
if(crumb) crumb.textContent=”Wikisource”;
if(rt) rt.textContent=(state.label||state.title).replace(/_/g,” “);
const secs=await apiParseSections(title);
setChapterOptions(secs, state.section);
const html=await apiParseText(title, state.section);
const blocks=htmlToBlocks(html);
const secText=chapter?.selectedOptions?.[0]?.textContent||””;
const base=(state.label||state.title).replace(/_/g,” “);
const refBase=(secText && secText!==”(Full text)”) ? (base+” — “+secText) : base;
renderBlocks(blocks, refBase);
}
function openModal(){
if(!modal) return;
modal.classList.add(“on”);
modal.setAttribute(“aria-hidden”,”false”);
}
function closeModal(){
if(!modal) return;
modal.classList.remove(“on”);
modal.setAttribute(“aria-hidden”,”true”);
}
function renderBookmarks(){
if(!modalBody) return;
if(!bookmarks.length){
modalBody.innerHTML = `
No bookmarks yet. Open a text and press +.
`;
return;
}
modalBody.innerHTML = bookmarks.map(bm=>{
const dt = new Date(bm.createdAt).toLocaleString();
return `
${esc(bm.label)} ${esc(dt)}
🗑
`;
}).join(“”);
}
function addBookmark(){
if(state.view!==”page”){ toast(“Open a text first”); return; }
const sec=chapter?.selectedOptions?.[0]?.textContent||””;
const base=(state.label||state.title).replace(/_/g,” “);
const label=(sec && sec!==”(Full text)”) ? (base+” — “+sec) : base;
const id=”bm_”+Math.random().toString(16).slice(2)+”_”+Date.now();
bookmarks.unshift({id, createdAt:new Date().toISOString(), state:{…state}, label});
bookmarks=bookmarks.slice(0,200);
saveBms();
toast(“Bookmark added”);
}
// Buttons (top bar)
root.addEventListener(“click”, (e) => {
const a = e.target.closest(“[data-trpk-action]”);
if (!a) return;
const act = a.getAttribute(“data-trpk-action”);
if (act === “theme”) {
theme = (theme === “dark”) ? “light” : “dark”;
localStorage.setItem(themeKey, theme);
setTheme(theme);
}
if (act === “bookmarks”) { renderBookmarks(); openModal(); }
if (act === “addbm”) addBookmark();
if (act === “closeModal”) closeModal();
});
search?.addEventListener(“input”, filterNav);
copyRefBtn?.addEventListener(“click”, () => {
copyText(currentRef()).then(()=>toast(“Copied reference”)).catch(()=>toast(“Copy failed”));
});
prevBtn?.addEventListener(“click”, () => {
if(state.view!==”page” || !chapter || chapter.options.length {
if(state.view!==”page” || !chapter || chapter.options.length {
if(state.view!==”page”) return;
state.section = chapter.value; saveState();
openPage(state.title, state.label, state.section).catch(showErr);
});
// Delegate clicks (nav + verse copy + bookmark modal)
root.addEventListener(“click”, (e) => {
const openBtn = e.target.closest(“[data-open]”);
if (openBtn) {
const payload = jparse(openBtn.getAttribute(“data-open”), null);
if (!payload) return;
if (payload.type===”collection” && payload.id===”dn”) openDn().catch(showErr);
if (payload.type===”book”) {
const b = LIBRARY.find(x=>x.id===payload.id && x.type===”book”);
if (b) openPage(b.page, b.title, “0”).catch(showErr);
}
if (payload.type===”page”) openPage(payload.title, payload.label, payload.section||”0″).catch(showErr);
return;
}
const copyBtn = e.target.closest(“[data-copy]”);
if (copyBtn) {
const n = copyBtn.getAttribute(“data-copy”);
const verseEl = root.querySelector(`[data-verse=”${CSS.escape(n)}”]`);
if (!verseEl) return;
const txt = collapse(verseEl.querySelector(“.trpk-vt”)?.textContent || “”);
const ref = verseEl.getAttribute(“data-ref”) || currentRef();
copyText(txt + “\n\n— ” + ref).then(()=>toast(“Copied verse + reference”)).catch(()=>toast(“Copy failed”));
return;
}
const bmOpen = e.target.closest(“[data-bm-open]”);
if (bmOpen) {
const id = bmOpen.getAttribute(“data-bm-open”);
const bm = bookmarks.find(x=>x.id===id);
if (bm) { closeModal(); openPage(bm.state.title, bm.state.label, bm.state.section).catch(showErr); }
return;
}
const bmDel = e.target.closest(“[data-bm-del]”);
if (bmDel) {
const id = bmDel.getAttribute(“data-bm-del”);
bookmarks = bookmarks.filter(x=>x.id!==id);
saveBms();
renderBookmarks();
toast(“Bookmark deleted”);
}
});
// Try to enable enhanced mode. If it fails, fallback remains visible.
(async () => {
try {
// Quick ping
const ping = await fetch(API + “?action=query&format=json&origin=*&meta=siteinfo&siprop=general”, {cache:”no-cache”});
if (!ping.ok) throw new Error(“Cannot reach Wikisource API (” + ping.status + “)”);
// Show enhanced app, hide fallback only AFTER ping success
if (app) { app.style.display=”block”; app.setAttribute(“aria-hidden”,”false”); }
if (fallback) fallback.style.display=”none”;
// Load library
await openLibrary();
// Restore last view
if (state.view===”collection” && state.id===”dn”) await openDn();
if (state.view===”page” && state.title) await openPage(state.title, state.label, state.section||”0″);
toast(“Reader loaded”);
} catch (e) {
// Keep fallback visible
showErr(e);
}
})();
})();