MediaWiki:Common.js: Difference between revisions
No edit summary Tag: Reverted |
No edit summary |
||
| (13 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/* | /* MediaWiki:Common.js */ | ||
(function () { | |||
var LS_SCRIPT_KEY = 'grantha_reader_script'; | |||
var currentScript = 'deva'; | |||
' | function devanagariToTamil(text) { | ||
var | var CONSONANTS = { | ||
'क': 'க', 'ख': 'க', 'ग': 'க', 'घ': 'க', 'ङ': 'ங', | |||
'च': 'ச', 'छ': 'ச', 'ज': 'ஜ', 'झ': 'ஜ', 'ञ': 'ஞ', | |||
'ट': 'ட', 'ठ': 'ட', 'ड': 'ட', 'ढ': 'ட', 'ण': 'ண', | |||
'त': 'த', 'थ': 'த', 'द': 'த', 'ध': 'த', 'न': 'ந', | |||
'प': 'ப', 'फ': 'ப', 'ब': 'ப', 'भ': 'ப', 'म': 'ம', | |||
'य': 'ய', 'र': 'ர', 'ल': 'ல', 'ळ': 'ள', 'व': 'வ', | |||
'श': 'ஶ', 'ष': 'ஷ', 'स': 'ஸ', 'ह': 'ஹ' | |||
}; | |||
var DIACRITICS = { | |||
'ा': 'ா', 'ि': 'ி', 'ी': 'ீ', 'ु': 'ு', 'ू': 'ூ', | |||
'े': 'ே', 'ै': 'ை', 'ो': 'ோ', 'ौ': 'ௌ' | |||
}; | |||
var VOWELS = { | |||
'अ': 'அ', 'आ': 'ஆ', 'इ': 'இ', 'ई': 'ஈ', 'उ': 'உ', 'ऊ': 'ஊ', | |||
'ए': 'ஏ', 'ऐ': 'ஐ', 'ओ': 'ஓ', 'औ': 'ஔ', 'ऽ': 'ऽ' | |||
}; | |||
var MISC = { | |||
'ं':'ம்', 'ः':':', 'ँ':'ம்', 'ॐ':'ௐ', | |||
'०': '0', '१': '1', '२': '2', '३': '3', '४': '4', | |||
'५': '5', '६': '6', '७': '7', '८': '8', '९': '9' | |||
}; | |||
var HALANTA = '्'; | |||
var chars = Array.from(text), result = '', i = 0; | |||
while (i < chars.length) { | |||
var ch = chars[i], next = chars[i + 1]; | |||
if (CONSONANTS[ch]) { | |||
var base = CONSONANTS[ch]; | |||
if (next === HALANTA) { result += base + '்'; i += 2; } | |||
else if (next === 'ृ') { result += base + '்ரி'; i += 2; } | |||
else if (next === 'ॄ') { result += base + '்ரீ'; i += 2; } | |||
else if (DIACRITICS[next]) { result += base + DIACRITICS[next]; i += 2; } | |||
else { result += base; i++; } // inherent 'a' | |||
} | } | ||
else if (ch === 'ऋ') { result += 'ரி'; i++; } | |||
else if (ch === 'ॠ') { result += 'ரீ'; i++; } | |||
else if (VOWELS[ch]) { result += VOWELS[ch]; i++; } | |||
else if (MISC[ch]) { result += MISC[ch]; i++; } | |||
else { result += ch; i++; } | |||
} | |||
return result; | |||
} | |||
if ( | |||
} | |||
function devanagariToIAST(text) { | |||
function | |||
var CONSONANTS = { | var CONSONANTS = { | ||
'क': ' | 'क': 'k', 'ख': 'kh', 'ग': 'g', 'घ': 'gh', 'ङ': 'ṅ', | ||
'च': ' | 'च': 'c', 'छ': 'ch', 'ज': 'j', 'झ': 'jh', 'ञ': 'ñ', | ||
'ट': ' | 'ट': 'ṭ', 'ठ': 'ṭh', 'ड': 'ḍ', 'ढ': 'ḍh', 'ण': 'ṇ', | ||
'त': ' | 'त': 't', 'थ': 'th', 'द': 'd', 'ध': 'dh', 'न': 'n', | ||
'प': ' | 'प': 'p', 'फ': 'ph', 'ब': 'b', 'भ': 'bh', 'म': 'm', | ||
'य': ' | 'य': 'y', 'र': 'r', 'ल': 'l', 'ळ': 'ḷ', 'व': 'v', | ||
'श': ' | 'श': 'ś', 'ष': 'ṣ', 'स': 's', 'ह': 'h' | ||
}; | }; | ||
var DIACRITICS = { | var DIACRITICS = { | ||
'ा': ' | 'ा': 'ā', 'ि': 'i', 'ी': 'ī', 'ु': 'u', 'ू': 'ū', | ||
'े': ' | 'ृ': 'ṛ', 'ॄ': 'ṝ', 'े': 'e', 'ै': 'ai', 'ो': 'o', 'ौ': 'au' | ||
}; | }; | ||
var VOWELS = { | var VOWELS = { | ||
'अ': ' | 'अ': 'a', 'आ': 'ā', 'इ': 'i', 'ई': 'ī', 'उ': 'u', 'ऊ': 'ū', | ||
'ए': ' | 'ऋ': 'ṛ', 'ॠ': 'ṝ', 'ए': 'e', 'ऐ': 'ai', 'ओ': 'o', 'औ': 'au', 'ऽ': "'" | ||
}; | }; | ||
var MISC = { | var MISC = { | ||
'ं':' | 'ं': 'ṃ', 'ः': 'ḥ', 'ँ': 'm̐', 'ॐ': 'oṃ', | ||
'०': '0', '१': '1', '२': '2', '३': '3', '४': '4', | '०': '0', '१': '1', '२': '2', '३': '3', '४': '4', | ||
'५': '5', '६': '6', '७': '7', '८': '8', '९': '9' | '५': '5', '६': '6', '७': '7', '८': '8', '९': '9' | ||
| Line 87: | Line 79: | ||
if (CONSONANTS[ch]) { | if (CONSONANTS[ch]) { | ||
var base = CONSONANTS[ch]; | var base = CONSONANTS[ch]; | ||
if (next === HALANTA) { result += base | if (next === HALANTA) { result += base; i += 2; } | ||
else if (DIACRITICS[next]) { result += base + DIACRITICS[next]; i += 2; } | else if (DIACRITICS[next]) { result += base + DIACRITICS[next]; i += 2; } | ||
else { result += base; i+ | else if (next === 'ं' || next === 'ः') { result += base + 'a' + MISC[next]; i += 2; } | ||
else { result += base + 'a'; i++; } | |||
} | } | ||
else if (VOWELS[ch]) { result += VOWELS[ch]; i++; } | else if (VOWELS[ch]) { result += VOWELS[ch]; i++; } | ||
else if (DIACRITICS[ch]) { result += DIACRITICS[ch]; i++; } | |||
else if (MISC[ch]) { result += MISC[ch]; i++; } | else if (MISC[ch]) { result += MISC[ch]; i++; } | ||
else { result += ch; i++; } | else { result += ch; i++; } | ||
| Line 102: | Line 92: | ||
} | } | ||
var SCRIPT_MAP = { | |||
kn: { | |||
'क': ' | 'अ': 'ಅ', 'आ': 'ಆ', 'इ': 'ಇ', 'ई': 'ಈ', 'उ': 'ಉ', 'ऊ': 'ಊ', 'ऋ': 'ಋ', 'ॠ': 'ೠ', | ||
'च': ' | 'ए': 'ಏ', 'ऐ': 'ಐ', 'ओ': 'ಓ', 'औ': 'ಔ', 'ऽ': 'ಽ', | ||
'ट': ' | 'क': 'ಕ', 'ख': 'ಖ', 'ग': 'ಗ', 'घ': 'ಘ', 'ङ': 'ಙ', | ||
'त': ' | 'च': 'ಚ', 'छ': 'ಛ', 'ज': 'ಜ', 'झ': 'ಝ', 'ञ': 'ಞ', | ||
'प': ' | 'ट': 'ಟ', 'ठ': 'ಠ', 'ड': 'ಡ', 'ढ': 'ಢ', 'ण': 'ಣ', | ||
'य': ' | 'त': 'ತ', 'थ': 'ಥ', 'द': 'ದ', 'ध': 'ಧ', 'न': 'ನ', | ||
'श': ' | 'प': 'ಪ', 'फ': 'ಫ', 'ब': 'ಬ', 'भ': 'ಭ', 'म': 'ಮ', | ||
'य': 'ಯ', 'र': 'ರ', 'ल': 'ಲ', 'ळ': 'ಳ', 'व': 'ವ', | |||
'श': 'ಶ', 'ष': 'ಷ', 'स': 'ಸ', 'ह': 'ಹ', | |||
'ा': ' | 'ा': 'ಾ', 'ि': 'ಿ', 'ी': 'ೀ', 'ु': 'ು', 'ू': 'ೂ', | ||
'ृ': ' | 'ृ': 'ೃ', 'ॄ': 'ೄ', 'े': 'ೇ', 'ै': 'ೈ', 'ो': 'ೋ', 'ौ': 'ೌ', | ||
} | 'ं': 'ಂ', 'ः': 'ಃ', '्': '್', 'ँ': 'ಁ', | ||
'०': '೦', '१': '೧', '२': '೨', '३': '೩', '४': '೪', | |||
'अ': ' | '५': '೫', '६': '೬', '७': '೭', '८': '೮', '९': '೯' | ||
}, | |||
te: { | |||
'अ': 'అ', 'आ': 'ఆ', 'इ': 'ఇ', 'ई': 'ఈ', 'उ': 'ఉ', 'ऊ': 'ఊ', 'ऋ': 'ఋ', 'ॠ': 'ౠ', | |||
' | 'ए': 'ఏ', 'ऐ': 'ఐ', 'ओ': 'ఓ', 'औ': 'ఔ', 'ऽ': 'ఽ', | ||
' | 'क': 'క', 'ख': 'ఖ', 'ग': 'గ', 'घ': 'ఘ', 'ङ': 'ఙ', | ||
' | 'च': 'చ', 'छ': 'ఛ', 'ज': 'జ', 'झ': 'ఝ', 'ञ': 'ఞ', | ||
'ट': 'ట', 'ठ': 'ఠ', 'ड': 'డ', 'ढ': 'ఢ', 'ण': 'ణ', | |||
'त': 'త', 'थ': 'థ', 'द': 'ద', 'ध': 'ధ', 'न': 'న', | |||
'प': 'ప', 'फ': 'ఫ', 'ब': 'బ', 'भ': 'భ', 'म': 'మ', | |||
'य': 'య', 'र': 'ర', 'ल': 'ల', 'ळ': 'ళ', 'व': 'వ', | |||
'श': 'శ', 'ष': 'ష', 'स': 'స', 'ह': 'హ', | |||
'ा': 'ా', 'ि': 'ి', 'ी': 'ీ', 'ु': 'ు', 'ू': 'ూ', | |||
'ृ': 'ృ', 'ॄ': 'ౄ', 'े': 'ే', 'ै': 'ై', 'ो': 'ో', 'ौ': 'ౌ', | |||
'ं': 'ం', 'ः': 'ః', '्': '్', 'ँ': 'ఁ', | |||
'०': '౦', '१': '౧', '२': '౨', '३': '౩', '४': '౪', | |||
'५': '౫', '६': '౬', '७': '౭', '८': '౮', '९': '౯' | |||
} | } | ||
}; | |||
} | |||
var PRE = [ | |||
var PRE = [ | |||
[/ङ्क/g, 'ंक'], [/ङ्ख/g, 'ंख'], [/ङ्ग/g, 'ंग'], [/ङ्घ/g, 'ंघ'], | [/ङ्क/g, 'ंक'], [/ङ्ख/g, 'ंख'], [/ङ्ग/g, 'ंग'], [/ङ्घ/g, 'ंघ'], | ||
[/ञ्च/g, 'ंच'], [/ञ्ज/g, 'ंज'], [/ण्ट/g, 'ंट'], [/ण्ड/g, 'ंड'], | [/ञ्च/g, 'ंच'], [/ञ्ज/g, 'ंज'], [/ण्ट/g, 'ंट'], [/ण्ड/g, 'ंड'], | ||
| Line 1,595: | Line 1,543: | ||
} | } | ||
function | // One request for ALL of them, not one each. The API takes up to 50 | ||
return $.ajax({ | // titles at a time, and a moola page now shows every commentary by | ||
// default - so what used to be one round trip became five or ten, | |||
// fired together but each with its own latency, and the commentaries | |||
// appeared well after the text. Batching makes it a single request | |||
// whatever the page carries. maxage/smaxage let the browser and the | |||
// CDN reuse the response, so moving between chapters of one grantha | |||
// does not re-fetch the same commentaries. | |||
var teekaJsonCache = {}; | |||
function fetchTeekaJsons(titles) { | |||
var want = titles.filter(function (t) { return !(t in teekaJsonCache); }); | |||
catch ( | if (!want.length) return $.Deferred().resolve(teekaJsonCache).promise(); | ||
}). | |||
var batches = []; | |||
for (var i = 0; i < want.length; i += 40) batches.push(want.slice(i, i + 40)); | |||
return $.when.apply($, batches.map(function (batch) { | |||
return $.ajax({ | |||
url: mw.util.wikiScript('api'), | |||
data: { | |||
action: 'query', | |||
titles: batch.map(function (t) { return 'JSON:' + t; }).join('|'), | |||
prop: 'revisions', rvslots: 'main', rvprop: 'content', | |||
format: 'json', formatversion: 2, | |||
maxage: 300, smaxage: 300 | |||
}, | |||
dataType: 'json' | |||
}).then(function (data) { | |||
var pages = (data.query && data.query.pages) || []; | |||
pages.forEach(function (page) { | |||
// Match the response back to the title we asked for: | |||
// the API may normalise or reorder, so never pair by | |||
// index. Strip the namespace the same way it was added. | |||
var title = (page.title || '').replace(/^JSON:/, ''); | |||
if (page.missing) { teekaJsonCache[title] = null; return; } | |||
var content = page.revisions && page.revisions[0] && | |||
page.revisions[0].slots && page.revisions[0].slots.main && | |||
page.revisions[0].slots.main.content; | |||
if (!content) { teekaJsonCache[title] = null; return; } | |||
try { | |||
var parsed = JSON.parse(content); | |||
parsed._title = title; | |||
teekaJsonCache[title] = parsed; | |||
} catch (e) { teekaJsonCache[title] = null; } | |||
}); | |||
}).catch(function () { | |||
batch.forEach(function (t) { if (!(t in teekaJsonCache)) teekaJsonCache[t] = null; }); | |||
}); | |||
})).then(function () { | |||
// Anything the response never mentioned is recorded as absent, | |||
// so a later call does not ask for it again. | |||
want.forEach(function (t) { if (!(t in teekaJsonCache)) teekaJsonCache[t] = null; }); | |||
return teekaJsonCache; | |||
}); | |||
} | } | ||
| Line 1,674: | Line 1,663: | ||
index[moolaId].push({ | index[moolaId].push({ | ||
teekaTitle: td.doc.title || td._title, | teekaTitle: td.doc.title || td._title, | ||
teekaAuthor: td.doc.author || '', | |||
teekaPage: td.doc.pageTitle || td._title, | |||
teekaGranthaId: td.doc.granthaId || '', | |||
teekaSlug: slug, | teekaSlug: slug, | ||
// join('\n') - each entry of `lines` is a printed line, | // join('\n') - each entry of `lines` is a printed line, | ||
| Line 1,707: | Line 1,699: | ||
.attr('data-teeka-slug', entry.teekaSlug) | .attr('data-teeka-slug', entry.teekaSlug) | ||
.attr('data-teeka-block-id', entry.blockId) | .attr('data-teeka-block-id', entry.blockId) | ||
// Citation metadata: a bookmark taken inside an | |||
// interleaved commentary must cite THAT work and its | |||
// author, and link to the commentary's own page - not | |||
// to the moola whose page it happens to be shown on. | |||
.attr('data-cite-author', entry.teekaAuthor || '') | |||
.attr('data-cite-title', entry.teekaTitle || '') | |||
.attr('data-cite-page', entry.teekaPage || '') | |||
.attr('data-cite-grantha-id', entry.teekaGranthaId || '') | |||
.attr('id', entry.blockId) | |||
.css('background', color); | .css('background', color); | ||
if (window.grLibrary) { | |||
(function (el, gid, page) { | |||
// The id may come from the teeka's own JSON (if that | |||
// document has been rebuilt) or from the map page. | |||
// Either way the commentary cites ITS work, not the | |||
// moola it is displayed inside. | |||
var got = gid ? Promise.resolve(gid) | |||
: window.grLibrary.idForPage(page, null); | |||
got.then(function (id) { | |||
if (!id) return null; | |||
el.setAttribute('data-cite-grantha-id', id); | |||
return window.grLibrary.lookup(id); | |||
}).then(function (g) { | |||
if (!g) return; | |||
if (g.name) el.setAttribute('data-cite-title', g.name); | |||
if (g.nameIAST) el.setAttribute('data-cite-title-iast', g.nameIAST); | |||
if (g.author) el.setAttribute('data-cite-author', g.author); | |||
if (g.authorIAST) el.setAttribute('data-cite-author-iast', g.authorIAST); | |||
var h = el.querySelector('.teeka-title'); | |||
var label = teekaLabel(g); | |||
if (h && label) h.textContent = label; | |||
var slug = el.getAttribute('data-teeka-slug'); | |||
if (slug && label) { | |||
labelBySlug[slug] = label; | |||
// A footnote section for this commentary may | |||
// already be on screen; rename it too. | |||
document.querySelectorAll( | |||
'.gr-teeka-notes-group[data-teeka-slug="' + slug + | |||
'"] .gr-teeka-notes-title').forEach(function (t) { | |||
t.textContent = label; | |||
}); | |||
} | |||
}); | |||
}($entry[0], entry.teekaGranthaId, entry.teekaPage)); | |||
} | |||
$('<div>').addClass('teeka-title').text(entry.teekaTitle).appendTo($entry); | $('<div>').addClass('teeka-title').text(entry.teekaTitle).appendTo($entry); | ||
$('<div>').addClass('teeka-body').html(entry.text).appendTo($entry); | $('<div>').addClass('teeka-body').html(entry.text).appendTo($entry); | ||
| Line 1,716: | Line 1,752: | ||
// them here, under the commentary's title, for the note | // them here, under the commentary's title, for the note | ||
// card and the right-hand panel to read. | // card and the right-hand panel to read. | ||
if (entry.footnotes && entry.footnotes.length) { | if (entry.footnotes && entry.footnotes.length && window.grFootnoteRegistry) { | ||
window.grFootnoteRegistry.add(entry.teekaSlug, entry.teekaTitle, entry.footnotes); | window.grFootnoteRegistry.add(entry.teekaSlug, entry.teekaTitle, entry.footnotes); | ||
} | } | ||
$wrap.append($entry); | $wrap.append($entry); | ||
}); | }); | ||
$verseEl.after($wrap); | // After the bhashya, not between the verse and it. The moola | ||
// page prints verse then bhashya as one unit, and inserting | |||
// straight after the verse element split them apart, putting | |||
// the commentaries ahead of the bhashya they comment on. | |||
// The bhashya-collection for a verse carries id | |||
// "bhashya-<verse oldKey>"; where a page has none, the verse | |||
// itself is still the anchor. | |||
var anchor = $verseEl; | |||
var coll = document.getElementById('bhashya-' + verseId); | |||
if (coll && coll.compareDocumentPosition($verseEl[0]) & Node.DOCUMENT_POSITION_PRECEDING) { | |||
anchor = $(coll); | |||
} else { | |||
// Some pages emit the bhashya as a sibling block without | |||
// the collection wrapper - walk forward over any blocks | |||
// that belong to this same verse. | |||
var n = $verseEl[0].nextElementSibling; | |||
while (n && n.getAttribute && | |||
(n.getAttribute('data-parent-verse') === verseId || | |||
n.id === 'bhashya-' + verseId)) { | |||
anchor = $(n); | |||
n = n.nextElementSibling; | |||
} | |||
} | |||
anchor.after($wrap); | |||
// Tell the script switcher about the new text, so an interleaved | // Tell the script switcher about the new text, so an interleaved | ||
// commentary is transliterated like the rest of the page instead | // commentary is transliterated like the rest of the page instead | ||
| Line 1,734: | Line 1,793: | ||
function removeTeekaInMoola() { | function removeTeekaInMoola() { | ||
$('.gr-injected-teeka-group').remove(); | $('.gr-injected-teeka-group').remove(); | ||
window.grFootnoteRegistry.clearInjected(); | if (window.grFootnoteRegistry) window.grFootnoteRegistry.clearInjected(); | ||
} | } | ||
| Line 1,813: | Line 1,872: | ||
// ── State ────────────────────────────────────────────────────── | // ── State ────────────────────────────────────────────────────── | ||
// Not seeded from storage: every page load starts with all | |||
// commentaries on (see where availableList is filled). Unchecking one | |||
// still writes to storage, which keeps the control's own state | |||
// consistent within the page and leaves the value there for anything | |||
// else that reads it, but it is not what decides the initial view. | |||
var selectedTeekaSlugs = {}; | var selectedTeekaSlugs = {}; | ||
var teekaIndex = null; | var teekaIndex = null; | ||
var availableList = []; // [{slug, title, subItems: [{slug, title}, ...]}] | var availableList = []; // [{slug, title, subItems: [{slug, title}, ...]}] | ||
| Line 1,945: | Line 2,003: | ||
findCommentarySubpages(rootTitle).then(function (subpages) { | findCommentarySubpages(rootTitle).then(function (subpages) { | ||
if (!subpages.length) { isReady = true; readyCallbacks.forEach(function (cb) { cb([]); }); return; } | if (!subpages.length) { isReady = true; readyCallbacks.forEach(function (cb) { cb([]); }); return; } | ||
fetchTeekaJsons(subpages).then(function (cache) { | |||
var fetched = | var fetched = subpages.map(function (t) { return cache[t] || null; }); | ||
// Pair title+doc together FIRST, then filter as one unit - | // Pair title+doc together FIRST, then filter as one unit - | ||
// filtering `docs` separately from `subpages` and passing | // filtering `docs` separately from `subpages` and passing | ||
| Line 2,009: | Line 2,067: | ||
// cancelled edit, without needing to re-fetch | // cancelled edit, without needing to re-fetch | ||
// anything from the server. | // anything from the server. | ||
// Every commentary is on at the start of EVERY page | |||
// load, not only a reader's first visit. The moola is | |||
// published together with its commentaries and the | |||
// complete text is what a reader should land on; the | |||
// checkboxes are for narrowing it down while reading, | |||
// and that narrowing lasts for as long as the page is | |||
// open rather than following the reader to the next one. | |||
// | |||
// This MUST come before augmentExistingTeekaLinks: | |||
// that builds each inline checkbox with | |||
// .prop('checked', !!selectedTeekaSlugs[slug]), reading | |||
// the selection as it stands at that moment. Selecting | |||
// afterwards left every box drawn unchecked while the | |||
// commentaries themselves were shown - the state and | |||
// the control disagreeing on screen. | |||
availableList.forEach(function (it) { | |||
selectedTeekaSlugs[it.slug] = true; | |||
(it.subItems || []).forEach(function (sub) { selectedTeekaSlugs[sub.slug] = true; }); | |||
}); | |||
lastAugmentArgs = { titles: pairs.map(function (p) { return p.title; }), docs: topDocs }; | lastAugmentArgs = { titles: pairs.map(function (p) { return p.title; }), docs: topDocs }; | ||
var augmented = augmentExistingTeekaLinks(lastAugmentArgs.titles, lastAugmentArgs.docs); | var augmented = augmentExistingTeekaLinks(lastAugmentArgs.titles, lastAugmentArgs.docs); | ||
if (augmented.length) observeInlineVisibility(augmented.map(function (a) { return a.el; })); | if (augmented.length) observeInlineVisibility(augmented.map(function (a) { return a.el; })); | ||
else inlineListVisible = false; // no inline checkboxes exist - toolbar control should always show as the only access point | else inlineListVisible = false; // no inline checkboxes exist - toolbar control should always show as the only access point | ||
if (Object.keys(selectedTeekaSlugs).length) renderInterleave(); | if (Object.keys(selectedTeekaSlugs).length) renderInterleave(); | ||
isReady = true; | isReady = true; | ||
| Line 2,031: | Line 2,098: | ||
if (!uniqueRelated.length) { finishInit({}); return; } | if (!uniqueRelated.length) { finishInit({}); return; } | ||
fetchTeekaJsons(uniqueRelated).then(function (cache) { | |||
var subDocsBySlug = {}; | var subDocsBySlug = {}; | ||
uniqueRelated.forEach(function (slug | uniqueRelated.forEach(function (slug) { | ||
if (! | if (!cache[slug]) console.warn('[grTeekaInterleave] Could not load sub-commentary JSON:' + slug); | ||
subDocsBySlug[slug] = | subDocsBySlug[slug] = cache[slug] || null; | ||
}); | }); | ||
finishInit(subDocsBySlug); | finishInit(subDocsBySlug); | ||
| Line 2,317: | Line 2,383: | ||
var since = new Date(Date.now() - WINDOW_DAYS * 86400000).toISOString().replace(/\.\d+Z$/, 'Z'); | var since = new Date(Date.now() - WINDOW_DAYS * 86400000).toISOString().replace(/\.\d+Z$/, 'Z'); | ||
var all = []; | var all = []; | ||
function page(cont) { | function page(cont, limit) { | ||
var p = { | var p = { | ||
action: 'query', list: 'recentchanges', rctype: 'new|edit', rcnamespace: 0, | action: 'query', list: 'recentchanges', rctype: 'new|edit', rcnamespace: 0, | ||
rcshow: '!redirect', rcprop: 'title|timestamp|sizes', rclimit: | rcshow: '!redirect', rcprop: 'title|timestamp|sizes', rclimit: limit, rcend: since | ||
}; | }; | ||
if (cont) p.rccontinue = cont; | if (cont) p.rccontinue = cont; | ||
return api(p).then(function (j) { | return api(p).then(function (j) { | ||
// The API answers 200 with an {error:...} body, so a failed | |||
// query is not a rejected promise. Treat it as one. | |||
if (j && j.error) throw new Error(j.error.code || 'api error'); | |||
all = all.concat((j.query && j.query.recentchanges) || []); | all = all.concat((j.query && j.query.recentchanges) || []); | ||
// Two pages (1,000 changes) is plenty for a notifications list. | // Two pages (1,000 changes) is plenty for a notifications list. | ||
if (j['continue'] && j['continue'].rccontinue && all.length < 1000) return page(j['continue'].rccontinue); | if (j['continue'] && j['continue'].rccontinue && all.length < 1000) { | ||
return page(j['continue'].rccontinue, limit); | |||
} | |||
return all; | |||
}).catch(function (err) { | |||
// recentchanges over a date range is the most expensive query | |||
// this site makes, and on a busy or constrained database it | |||
// can come back as a DBQueryError. Halve the window and retry | |||
// once; failing that, return nothing. A "what's new" list is | |||
// not worth breaking the page for. | |||
if (limit > 100) return page(cont, 100); | |||
try { | |||
console.warn('[gr] recent changes unavailable: ' + | |||
(err && err.message || err)); | |||
} catch (e) { } | |||
return all; | return all; | ||
}); | }); | ||
} | } | ||
return page(null); | return page(null, 500); | ||
} | } | ||
| Line 2,402: | Line 2,485: | ||
} catch (e) { } | } catch (e) { } | ||
} | } | ||
return Promise.all([fetchChanges(), fetchCatalogue()]).then(function (r) { | return Promise.all([ | ||
fetchChanges().catch(function () { return []; }), | |||
fetchCatalogue().catch(function () { return null; }) | |||
]).then(function (r) { | |||
return dropMissing(summarise(r[0], r[1]).slice(0, MAX_ITEMS + 10)); | return dropMissing(summarise(r[0], r[1]).slice(0, MAX_ITEMS + 10)); | ||
}).then(function (items) { | }).then(function (items) { | ||
| Line 2,635: | Line 2,721: | ||
// ── Footnote markers and their cards ──────────────────────────────── | // ── Footnote markers and their cards ──────────────────────────────── | ||
/* | /* ── FOOTNOTE REGISTRY ──────────────────────────────────────────────── | ||
Notes that belong to content injected into this page rather than baked | |||
into it - an interleaved commentary's own footnotes live in ITS | |||
document, so nothing on the moola page knew what its highlighted words | |||
pointed at. Anything registered here is visible to the note card and to | |||
the right-hand panel, grouped under the commentary it came from. */ | |||
window.grFootnoteRegistry = (function () { | |||
'use strict'; | |||
at | var byId = {}; // gra-id -> {text, quote, type, group, groupTitle} | ||
var groups = []; // [{slug, title}] in the order they were added | |||
var listeners = []; | |||
var pending = null; | |||
// Deferred by a tick on purpose. Notes are registered while a | |||
// commentary's block is still being built, BEFORE it is inserted into | |||
// the page - so a listener that re-scans the document synchronously | |||
(function () { | // would run against a DOM that does not contain the highlighted words | ||
'use strict'; | // yet, and find nothing to wire up. Coalescing to one call after the | ||
// current task also means a page with ten commentaries re-scans once | |||
var | // rather than ten times. | ||
var | function notify() { | ||
// | if (pending) return; | ||
// | pending = setTimeout(function () { | ||
pending = null; | |||
listeners.forEach(function (f) { try { f(); } catch (e) {} }); | |||
}, 0); | |||
} | |||
return { | |||
add: function (slug, title, notes) { | |||
if (!groups.some(function (g) { return g.slug === slug; })) { | |||
groups.push({ slug: slug, title: title }); | |||
} | |||
var added = false; | |||
(notes || []).forEach(function (n) { | |||
if (!n || !n.id || byId[n.id]) return; | |||
byId[n.id] = { | |||
text: n.text || '', quote: n.quote || '', | |||
type: n.kind || n.type || 'note', | |||
group: slug, groupTitle: title | |||
}; | |||
added = true; | |||
}); | |||
if (added) notify(); | |||
}, | |||
get: function (id) { return byId[id] || null; }, | |||
all: function () { return byId; }, | |||
groups: function () { return groups.slice(); }, | |||
isInjected: function (id) { return !!byId[id]; }, | |||
clearInjected: function () { byId = {}; groups = []; notify(); }, | |||
onChange: function (f) { listeners.push(f); } | |||
}; | }; | ||
}()); | |||
/* QuickEdit already stores footnotes: it wraps the annotated words in | |||
<span class="gra-qe-footnote-highlight" data-gra-id="…"> and appends a | |||
<div class="gra-qe-footnotes"><ol> at the foot of the document, one | |||
<li data-gra-id="…"> per note carrying the quoted words and the note. | |||
function notesFromList() { | That is the whole apparatus, but reading it meant scrolling to the end of | ||
var out = {}; | a long document and scrolling back. This turns the same data into what | ||
var reg = window.grFootnoteRegistry.all(); | Setutila shows: a small superscript after the annotated words, and a card | ||
Object.keys(reg).forEach(function (id) { | at the point of reading. Nothing is stored differently and QuickEdit is | ||
untouched — the list is still written, still saved, and still printed; it | |||
is only taken off the screen, where the markers replace it. | |||
The note text is transliterated with the rest of the page: the card's body | |||
is tagged data-deva and announced through gr-new-content, which the script | |||
switcher already listens for. */ | |||
(function () { | |||
'use strict'; | |||
var HL = '.gra-qe-footnote-highlight'; | |||
var LIST = '.gra-qe-footnotes'; | |||
// data-gra-type on the <li>. The names the converter writes, and the | |||
// ones a QuickEdit button could write later, land in the same map. | |||
var TYPES = { | |||
pathantara: 'पाठान्तरम्', // a variant reading | |||
locus: 'प्रमाणम्', // where a quotation is from — ऋ.सं.७.९९ | |||
source: 'प्रमाणम्', | |||
note: 'टिप्पणी' // an editorial remark | |||
}; | |||
var card = null, openFor = null; | |||
function notesFromList() { | |||
var out = {}; | |||
var reg = window.grFootnoteRegistry.all(); | |||
Object.keys(reg).forEach(function (id) { | |||
out[id] = { text: reg[id].text, quote: reg[id].quote, | out[id] = { text: reg[id].text, quote: reg[id].quote, | ||
type: reg[id].type, injected: true, | type: reg[id].type, injected: true, | ||
| Line 2,717: | Line 2,855: | ||
openFor.setAttribute('aria-expanded', 'false'); | openFor.setAttribute('aria-expanded', 'false'); | ||
openFor.classList.remove('gr-note-marker-open'); | openFor.classList.remove('gr-note-marker-open'); | ||
openFor.classList.remove('gr-note-hl-open'); | |||
} | } | ||
openFor = null; | openFor = null; | ||
| Line 2,755: | Line 2,894: | ||
} catch (e) { } | } catch (e) { } | ||
marker.setAttribute('aria-expanded', 'true'); | marker.setAttribute('aria-expanded', 'true'); | ||
marker.classList.add('gr-note-marker-open'); | // A commentary's note has no superscript, so the element the card is | ||
// anchored to IS the highlighted word. gr-note-marker-open paints a | |||
// solid maroon chip with white text - right for a small superscript, | |||
// and it made the word itself unreadable. The word gets its own, | |||
// quieter open state instead. | |||
marker.classList.add(marker.classList.contains('gr-note-marker') | |||
? 'gr-note-marker-open' : 'gr-note-hl-open'); | |||
openFor = marker; | openFor = marker; | ||
} | } | ||
| Line 2,873: | Line 3,018: | ||
wrap.setAttribute('data-teeka-slug', g.slug); | wrap.setAttribute('data-teeka-slug', g.slug); | ||
var h = document.createElement('div'); | var h = document.createElement('div'); | ||
h.className = 'gr-teeka-notes-title'; | h.className = 'gr-teeka-notes-title'; | ||
h.textContent = g.title || g.slug; | h.textContent = (window.grLibrary && window.grLibrary.labelFor(g.slug)) | ||
wrap.appendChild(h); | || g.title || g.slug; | ||
var ol = document.createElement('ol'); | wrap.appendChild(h); | ||
ids.forEach(function (id) { | var ol = document.createElement('ol'); | ||
var n = reg[id]; | ids.forEach(function (id) { | ||
var li = document.createElement('li'); | var n = reg[id]; | ||
li.className = 'gra-qe-footnote-item'; | var li = document.createElement('li'); | ||
li.setAttribute('data-gra-id', id); | li.className = 'gra-qe-footnote-item'; | ||
li.setAttribute('data-gra-type', n.type); | li.setAttribute('data-gra-id', id); | ||
if (n.quote) { | li.setAttribute('data-gra-type', n.type); | ||
var q = document.createElement('span'); | if (n.quote) { | ||
q.className = 'gra-qe-footnote-quote'; | var q = document.createElement('span'); | ||
q.textContent = n.quote; | q.className = 'gra-qe-footnote-quote'; | ||
li.appendChild(q); | q.textContent = n.quote; | ||
} | li.appendChild(q); | ||
var t = document.createElement('span'); | } | ||
t.className = 'gra-qe-footnote-text'; | var t = document.createElement('span'); | ||
t.textContent = n.text; | t.className = 'gra-qe-footnote-text'; | ||
li.appendChild(t); | t.textContent = n.text; | ||
ol.appendChild(li); | li.appendChild(t); | ||
}); | ol.appendChild(li); | ||
wrap.appendChild(ol); | }); | ||
host.appendChild(wrap); | wrap.appendChild(ol); | ||
}); | host.appendChild(wrap); | ||
try { | }); | ||
window.dispatchEvent(new CustomEvent('gr-new-content', { detail: { container: host } })); | try { | ||
} catch (e) { } | window.dispatchEvent(new CustomEvent('gr-new-content', { detail: { container: host } })); | ||
} | } catch (e) { } | ||
} | |||
window.grFootnoteRegistry.onChange(function () { | |||
publishInjectedList(); | window.grFootnoteRegistry.onChange(function () { | ||
decorate(); | publishInjectedList(); | ||
decorate(); | |||
}); | |||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', decorate); | |||
else decorate(); | |||
if (window.mw && mw.hook) mw.hook('wikipage.content').add(function () { setTimeout(decorate, 150); }); | |||
}()); | |||
/* ── HASH REVEAL ─────────────────────────────────────────────────────── | |||
A copied id or a bookmark citation is only useful if its #link actually | |||
lands on the block. The browser's own fragment scroll fires once, at | |||
parse time, and then our fonts swap in and the teeka blocks inject - | |||
the page grows under the reader and the target drifts off screen. It | |||
looks exactly like the link didn't work. | |||
This retries until the element exists and re-settles after late content | |||
arrives. It used to live in copyId.js (Editor extension), which is no | |||
longer loaded on this wiki, so it moves here where it always runs. If | |||
copyId IS loaded it does the same job; both are idempotent, and the | |||
guard below keeps them from fighting over the same jump. */ | |||
(function () { | |||
'use strict'; | |||
if (window.grHashReveal) return; | |||
var FLASH_MS = 2600; | |||
var _last = null; | |||
function findTarget(id) { | |||
if (!id) return null; | |||
var esc = (window.CSS && CSS.escape) ? CSS.escape(id) : id.replace(/["\\]/g, '\\$&'); | |||
return document.getElementById(id) || | |||
document.querySelector('[data-block-id="' + esc + '"]') || | |||
document.querySelector('[data-copyid="' + esc + '"]') || | |||
null; | |||
} | |||
function currentHash() { | |||
var raw = (location.hash || '').slice(1); | |||
if (!raw) return ''; | |||
try { return decodeURIComponent(raw); } catch (e) { return raw; } | |||
} | |||
function land(el, flash) { | |||
el.scrollIntoView({ block: 'center', behavior: 'smooth' }); | |||
if (!flash) return; | |||
el.classList.add('gid-target'); | |||
setTimeout(function () { el.classList.remove('gid-target'); }, FLASH_MS); | |||
} | |||
function reveal(attemptsLeft, flash) { | |||
var id = currentHash(); | |||
if (!id) return; | |||
var el = findTarget(id); | |||
if (!el) { | |||
if (attemptsLeft > 0) setTimeout(function () { reveal(attemptsLeft - 1, flash); }, 400); | |||
return; | |||
} | |||
_last = id; | |||
land(el, flash); | |||
// Re-settle once the late arrivals (fonts, injected teekas) have | |||
// shifted the layout. Cheap, and it is what makes the link feel | |||
// like it worked rather than like it missed. | |||
setTimeout(function () { | |||
if (currentHash() !== id) return; | |||
var again = findTarget(id); | |||
if (again) again.scrollIntoView({ block: 'center', behavior: 'auto' }); | |||
}, 1200); | |||
} | |||
function onLateContent() { | |||
var id = currentHash(); | |||
if (!id || id !== _last) return; | |||
var el = findTarget(id); | |||
if (el) el.scrollIntoView({ block: 'center', behavior: 'auto' }); | |||
} | |||
window.grHashReveal = function (attempts) { reveal(attempts || 12, true); }; | |||
function boot() { | |||
reveal(12, true); | |||
window.addEventListener('hashchange', function () { reveal(6, true); }); | |||
window.addEventListener('gr-new-content', onLateContent); | |||
if (document.fonts && document.fonts.ready && document.fonts.ready.then) { | |||
document.fonts.ready.then(onLateContent); | |||
} | |||
} | |||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); | |||
else boot(); | |||
}()); | |||
/* ── GRANTHA LIBRARY ─────────────────────────────────────────────────── | |||
The colophon in the source docx says "श्रीमद्वादिराजपूज्यचरणविरचिता | |||
प्रकाशिका" - an author phrase welded to a generic label. That is not a | |||
citable title, and it is what the client saw as "Vadirajathirtha | |||
virachitha". The Grantha library holds the real title and author, so | |||
the page carries only the library's stable id (data-grantha-id) and we | |||
resolve the rest here, at read time. Correct a name in the library and | |||
every citation on the site is correct on the next load - nothing to | |||
rebuild, nothing to re-upload. | |||
Reads only, and the library's GET endpoints are public: no API key is | |||
sent from the browser, and none may ever be put in this file, which | |||
any reader can fetch. */ | |||
(function () { | |||
'use strict'; | |||
if (window.grLibrary) return; | |||
// ---- CONFIGURE ME ------------------------------------------------- | |||
// Where the Grantha library's REST API lives. Same-origin by default, | |||
// which needs no CORS - but the library is its own app, so unless it is | |||
// reverse-proxied under the wiki's domain this must be its real host: | |||
// var API_BASE = 'https://library.example.org/api/granthas'; | |||
// On another host the library must send | |||
// Access-Control-Allow-Origin: https://anandamakaranda.in | |||
// or the browser will refuse the reply and every citation will fall | |||
// back to the author baked into the page (usually none). | |||
var API_BASE = window.GR_LIBRARY_API || '/api/granthas'; | |||
// Set to false while the library is not yet reachable: no request is | |||
// made at all, and citations use whatever the page itself carries. | |||
// Flip it back to true once API_BASE resolves. | |||
var LIBRARY_ENABLED = true; | |||
// ------------------------------------------------------------------- | |||
var API = API_BASE.replace(/\/+$/, ''); | |||
var mem = {}; | |||
var warned = false; | |||
// Once the library has answered "not here", every further call will get | |||
// the same answer. Stop asking: a wrong or missing API base otherwise | |||
// fills the network log with one 404 per work on the page. | |||
var libraryDown = !LIBRARY_ENABLED; | |||
// In memory only, for the life of the page. Names used to be kept in | |||
// sessionStorage for six hours, which quietly outlived any correction | |||
// made in the library or on the map page. They now come from the map | |||
// page, which is read once per load, so there is nothing to gain from | |||
// a second cache that can disagree with it. | |||
function cacheGet(id) { return mem[id] || null; } | |||
function cacheSet(id, v) { mem[id] = v; } | |||
// The library returns camelCase fields; normalise to what the citation | |||
// builder wants and keep BOTH scripts, since a Sanskrit reference is | |||
// cited in Devanagari and an English-language one in IAST. | |||
function shape(g) { | |||
if (!g) return null; | |||
return { | |||
id: g.id || '', | |||
name: g.name || '', | |||
nameIAST: g.transliteration || '', | |||
author: g.author || '', | |||
authorIAST: g.authorTransliteration || g.author_transliteration || '', | |||
category: g.category || '', | |||
prasthana: g.prasthana || '' | |||
}; | |||
} | |||
var inflight = {}; | |||
function lookup(id) { | |||
if (!id) return Promise.resolve(null); | |||
var hit = cacheGet(id); | |||
if (hit) return Promise.resolve(hit); | |||
// The map page carries the names for every work this wiki uses, so | |||
// normally nothing is fetched from the library at all. That matters | |||
// because the library is someone else's server: we cannot add CORS | |||
// headers to it, and a cross-origin call we cannot configure is a | |||
// call that will not work. The live API stays as an enhancement for | |||
// whenever it IS reachable. | |||
return granthaMap().then(function (m) { | |||
var w = (m.works || {})[id]; | |||
if (w && (w.name || w.nameIAST)) { | |||
var v = { id: id, name: w.name || '', nameIAST: w.nameIAST || '', | |||
author: w.author || '', authorIAST: w.authorIAST || '', | |||
category: w.category || '', prasthana: w.prasthana || '' }; | |||
cacheSet(id, v); | |||
return v; | |||
} | |||
return fetchFromLibrary(id); | |||
}); | |||
} | |||
function fetchFromLibrary(id) { | |||
if (libraryDown) return Promise.resolve(null); | |||
if (inflight[id]) return inflight[id]; | |||
inflight[id] = fetch(API + '/' + encodeURIComponent(id), { | |||
credentials: 'omit', headers: { 'Accept': 'application/json' } | |||
}).then(function (r) { | |||
// A 404 or 5xx means the base URL is wrong or the service is | |||
// down - not that this one work is missing - so trip the breaker. | |||
if (!r.ok) { | |||
if (r.status === 404 || r.status >= 500) libraryDown = true; | |||
throw new Error('library ' + r.status); | |||
} | |||
return r.json(); | |||
}).then(function (j) { | |||
var v = shape(j && (j.grantha || j.data || j)); | |||
if (v && v.name) cacheSet(id, v); | |||
delete inflight[id]; | |||
return v; | |||
}).catch(function (err) { | |||
// Offline, blocked by CORS, wrong host, or the id is not in the | |||
// library yet. The page's own data-author/data-doc-title stand | |||
// in, so a citation is never blocked on this request - but an | |||
// unreachable library looks exactly like a work with no author, | |||
// so say which it is, once. | |||
delete inflight[id]; | |||
libraryDown = true; | |||
if (!warned) { | |||
warned = true; | |||
try { | |||
console.warn('[grantha] could not read the library at ' + API + | |||
' (asked for "' + id + '"): ' + (err && err.message || err) + | |||
'. Citations will use whatever the page itself carries. ' + | |||
'Set window.GR_LIBRARY_API to the library host, and allow ' + | |||
'this origin via Access-Control-Allow-Origin.'); | |||
} catch (e) { } | |||
} | |||
return null; | |||
}); | |||
return inflight[id]; | |||
} | |||
// ── WHICH GRANTHA IS THIS PAGE? ──────────────────────────────────── | |||
// A page can say so itself (data-grantha-id, written by the pipeline), | |||
// but that would mean re-importing every document just to add one | |||
// attribute - and a re-import risks losing edits made on the site | |||
// since. So the mapping also lives in ONE wiki page, keyed by page | |||
// name, and the page looks itself up. Adding a document to the | |||
// library is then a single edit to that one page, with no content | |||
// touched anywhere. | |||
var MAP_PAGE = window.GR_GRANTHA_MAP_PAGE || 'MediaWiki:GranthaMap.json'; | |||
var mapPromise = null; | |||
function granthaMap() { | |||
if (mapPromise) return mapPromise; | |||
// Deliberately NOT cached in sessionStorage. It was, for six hours, | |||
// which meant that editing the map page changed nothing for anyone | |||
// whose tab had already read the old one - the exact failure you hit | |||
// while setting this up. mapPromise already collapses it to one | |||
// request per page load, and the browser's own HTTP cache handles | |||
// the rest; a stale map is far more expensive than a small fetch. | |||
try { sessionStorage.removeItem('gr-lib:map'); } catch (e) { } | |||
var url = (window.mw && mw.util && mw.util.getUrl) | |||
? mw.util.getUrl(MAP_PAGE, { action: 'raw', ctype: 'application/json' }) | |||
: '/index.php?title=' + encodeURIComponent(MAP_PAGE) + '&action=raw'; | |||
mapPromise = fetch(url, { credentials: 'same-origin' }) | |||
.then(function (r) { return r.ok ? r.json() : {}; }) | |||
.then(function (m) { | |||
// Either a flat {page: id} object, or {pages, works, | |||
// locators} when names and division names are recorded too. | |||
m = m || {}; | |||
if (!m.pages) m = { pages: m }; | |||
m.works = m.works || {}; | |||
m.locators = m.locators || {}; | |||
try { | |||
if (!Object.keys(m.works).length) { | |||
console.info('[grantha] the map page has no "works" section, ' + | |||
'so names must come from the library API. Re-run ' + | |||
'sync_grantha_map.py to embed them.'); | |||
} | |||
} catch (e) { } | |||
return m; | |||
}) | |||
.catch(function () { return {}; }); // no map page yet - fine | |||
return mapPromise; | |||
} | |||
// page name -> library id, however it is known. The attribute wins, | |||
// so a rebuilt page needs no map entry and a map entry needs no rebuild. | |||
function idForPage(pageName, el) { | |||
var own = el && el.getAttribute && el.getAttribute('data-grantha-id'); | |||
if (own) return Promise.resolve(own); | |||
if (!pageName) return Promise.resolve(''); | |||
return granthaMap().then(function (m) { | |||
var p = m.pages || {}; | |||
return p[pageName] || p[String(pageName).replace(/_/g, ' ')] || ''; | |||
}); | |||
} | |||
// Resolve this page and hand the result to anything listening (the | |||
// citation builder). Also fills in the visible title when the library | |||
// and the page disagree, so the reader sees the real name. | |||
function resolvePage() { | |||
var t = document.querySelector('.gr-doc-title'); | |||
if (!t) return; | |||
var page = (window.mw && mw.config.get('wgPageName')) || ''; | |||
// Division names (adhyaya / valli / mantra) come from the same | |||
// map page, so a locator can be spelled out without a rebuild too. | |||
if (!t.getAttribute('data-locator-scheme')) { | |||
granthaMap().then(function (m) { | |||
var sc = (m.locators || {})[page] || | |||
(m.locators || {})[String(page).replace(/_/g, ' ')]; | |||
if (sc) t.setAttribute('data-locator-scheme', sc); | |||
}); | |||
} | |||
idForPage(page, t).then(function (gid) { | |||
if (!gid) return; | |||
t.setAttribute('data-grantha-id', gid); | |||
return lookup(gid).then(function (g) { | |||
if (!g) return; | |||
// On a commentary's own page the heading still reads as the | |||
// docx colophon ("...विरचिता प्रकाशिका"). Name it the way the | |||
// blocks are named instead. | |||
if (t.getAttribute('data-parent-doc')) { | |||
var lab = teekaLabel(g); | |||
if (lab) t.textContent = lab; | |||
} else if (g.name) { | |||
t.textContent = g.name; | |||
} | |||
if (g.name) t.setAttribute('data-doc-title', g.name); | |||
if (g.nameIAST) t.setAttribute('data-doc-title-iast', g.nameIAST); | |||
if (g.author) t.setAttribute('data-author', g.author); | |||
if (g.authorIAST) t.setAttribute('data-author-iast', g.authorIAST); | |||
try { | |||
window.dispatchEvent(new CustomEvent('gr-library-resolved', { detail: g })); | |||
} catch (e) { } | |||
}); | |||
}); | |||
} | |||
// Fetching the whole catalogue is one request; fetching fifty records | |||
// one at a time is fifty. Past a handful, take the catalogue. | |||
var cataloguePromise = null; | |||
function catalogue() { | |||
if (libraryDown) return Promise.resolve({}); | |||
if (cataloguePromise) return cataloguePromise; | |||
cataloguePromise = fetch(API + '?limit=1000', { | |||
credentials: 'omit', headers: { 'Accept': 'application/json' } | |||
}).then(function (r) { | |||
if (!r.ok) { libraryDown = true; return null; } | |||
return r.json(); | |||
}) | |||
.then(function (j) { | |||
var list = (j && (j.granthas || j.data || j.results)) || []; | |||
var by = {}; | |||
list.forEach(function (g) { var v = shape(g); if (v && v.id) { by[v.id] = v; cacheSet(v.id, v); } }); | |||
return by; | |||
}).catch(function () { return {}; }); | |||
return cataloguePromise; | |||
} | |||
function lookupMany(ids) { | |||
var want = [], seen = {}; | |||
(ids || []).forEach(function (i) { if (i && !seen[i]) { seen[i] = 1; want.push(i); } }); | |||
// lookup() answers from the map page without a network call, so | |||
// resolving fifty works costs one shared map fetch, not fifty | |||
// requests. Only ids the map does not name reach the library. | |||
return Promise.all(want.map(lookup)).then(function (vals) { | |||
var out = {}; | |||
want.forEach(function (i, n) { out[i] = vals[n]; }); | |||
return out; | |||
}); | |||
} | |||
// How a commentary is named on screen: its author, then the work. | |||
// "श्रीवादिराजतीर्थः: काठकोपनिषद्भाष्यप्रकाशिका" - because several | |||
// commentaries on one moola share a work-type name (प्रकाशिका, | |||
// विवरणम्, खण्डार्थः), and the author is what actually tells them | |||
// apart for a reader scanning down the page. | |||
var labelBySlug = {}; | |||
function teekaLabel(g) { | |||
if (!g) return ''; | |||
var name = g.name || g.nameIAST || ''; | |||
var author = g.author || g.authorIAST || ''; | |||
if (!author || !name) return name || author; | |||
// Nothing to prefix if the work already opens with the author. | |||
if (name.indexOf(author) === 0) return name; | |||
return author + ': ' + name; | |||
} | |||
// ── DOCUMENT LINKS ──────────────────────────────────────────────── | |||
// The home page's cards were written by hand and carry the same | |||
// colophon labels the citation had to stop using - "प्रकाशिका", | |||
// "भाष्यम्" - which say what KIND of work it is, not which one. Any | |||
// link to a mapped page gets the library's name instead, so the | |||
// catalogue and the site agree without the page being edited. | |||
function relabelDocLinks(root) { | |||
var scope = root || document; | |||
var links = scope.querySelectorAll('.gr-home-grid a, .gr-doc-nav a, .gr-vadavali a'); | |||
if (!links.length) return; | |||
granthaMap().then(function (m) { | |||
var pages = m.pages || {}; | |||
var jobs = []; | |||
Array.prototype.forEach.call(links, function (a) { | |||
if (a.getAttribute('data-gr-relabelled')) return; | |||
var t = a.getAttribute('title') || | |||
decodeURIComponent((a.getAttribute('href') || '').split('#')[0] | |||
.replace(/^.*\/(?:wiki\/)?/, '')); | |||
var gid = pages[t] || pages[String(t).replace(/_/g, ' ')]; | |||
if (gid) jobs.push([a, gid]); | |||
}); | |||
if (!jobs.length) return; | |||
lookupMany(jobs.map(function (j) { return j[1]; })).then(function (by) { | |||
jobs.forEach(function (j) { | |||
var g = by[j[1]]; | |||
if (!g || !g.name) return; | |||
j[0].setAttribute('data-gr-relabelled', '1'); | |||
j[0].setAttribute('data-grantha-id', j[1]); | |||
if (g.authorIAST || g.author) { | |||
j[0].setAttribute('title', (g.author || '') + ' — ' + g.name); | |||
} | |||
j[0].textContent = g.name; | |||
}); | |||
}); | |||
}); | |||
} | |||
window.grLibraryVersion = '2026-09-30a'; | |||
window.grLibrary = { lookup: lookup, lookupMany: lookupMany, catalogue: catalogue, | |||
teekaLabel: teekaLabel, | |||
labelFor: function (slug) { return labelBySlug[slug] || ''; }, | |||
idForPage: idForPage, map: granthaMap, api: API, | |||
relabelDocLinks: relabelDocLinks }; | |||
function boot() { resolvePage(); relabelDocLinks(); } | |||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); | |||
else boot(); | |||
window.addEventListener('gr-new-content', function (e) { | |||
relabelDocLinks(e && e.detail && e.detail.container); | |||
}); | }); | ||
}()); | }()); | ||