MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
 
(5 intermediate revisions by the same user not shown)
Line 1,728: Line 1,728:
                             if (g.authorIAST) el.setAttribute('data-cite-author-iast', g.authorIAST);
                             if (g.authorIAST) el.setAttribute('data-cite-author-iast', g.authorIAST);
                             var h = el.querySelector('.teeka-title');
                             var h = el.querySelector('.teeka-title');
                             if (h && g.name) h.textContent = g.name;
                            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));
                     }($entry[0], entry.teekaGranthaId, entry.teekaPage));
Line 2,371: 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: 500, rcend: since
                 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,456: 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,987: Line 3,019:
             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))
                            || g.title || g.slug;
             wrap.appendChild(h);
             wrap.appendChild(h);
             var ol = document.createElement('ol');
             var ol = document.createElement('ol');
Line 3,125: Line 3,158:
     if (window.grLibrary) return;
     if (window.grLibrary) return;


     // Same-origin by default so no CORS is needed. Override before this
     // ---- CONFIGURE ME -------------------------------------------------
     // file loads if the library app lives on another host.
    // Where the Grantha library's REST API lives. Same-origin by default,
     var API = (window.GR_LIBRARY_API || '/api/granthas').replace(/\/+$/, '');
    // which needs no CORS - but the library is its own app, so unless it is
    var TTL = 6 * 60 * 60 * 1000;                // names change rarely
    // 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 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;


     function cacheGet(id) {
     // In memory only, for the life of the page. Names used to be kept in
        if (mem[id]) return mem[id];
    // sessionStorage for six hours, which quietly outlived any correction
        try {
    // made in the library or on the map page. They now come from the map
            var raw = sessionStorage.getItem('gr-lib:' + id);
    // page, which is read once per load, so there is nothing to gain from
            if (!raw) return null;
    // a second cache that can disagree with it.
            var o = JSON.parse(raw);
    function cacheGet(id) { return mem[id] || null; }
            if (!o || (Date.now() - o.t) > TTL) return null;
     function cacheSet(id, v) { mem[id] = v; }
            mem[id] = o.v;
            return o.v;
        } catch (e) { return null; }
    }
     function cacheSet(id, v) {
        mem[id] = v;
        try { sessionStorage.setItem('gr-lib:' + id, JSON.stringify({ t: Date.now(), v: v })); }
        catch (e) { }
    }


     // The library returns camelCase fields; normalise to what the citation
     // The library returns camelCase fields; normalise to what the citation
Line 3,169: Line 3,210:
         var hit = cacheGet(id);
         var hit = cacheGet(id);
         if (hit) return Promise.resolve(hit);
         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];
         if (inflight[id]) return inflight[id];
         inflight[id] = fetch(API + '/' + encodeURIComponent(id), {
         inflight[id] = fetch(API + '/' + encodeURIComponent(id), {
             credentials: 'omit', headers: { 'Accept': 'application/json' }
             credentials: 'omit', headers: { 'Accept': 'application/json' }
         }).then(function (r) {
         }).then(function (r) {
             if (!r.ok) throw new Error('library ' + r.status);
            // 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();
             return r.json();
         }).then(function (j) {
         }).then(function (j) {
Line 3,180: Line 3,247:
             delete inflight[id];
             delete inflight[id];
             return v;
             return v;
         }).catch(function () {
         }).catch(function (err) {
             // Offline, blocked, or the id is not in the library yet. The
             // Offline, blocked by CORS, wrong host, or the id is not in the
            // page's own data-author/data-doc-title stand in; a citation
            // library yet. The page's own data-author/data-doc-title stand
             // is never blocked on this request.
             // 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];
             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 null;
         });
         });
Line 3,203: Line 3,283:
     function granthaMap() {
     function granthaMap() {
         if (mapPromise) return mapPromise;
         if (mapPromise) return mapPromise;
         var cached = null;
         // Deliberately NOT cached in sessionStorage. It was, for six hours,
         try {
        // which meant that editing the map page changed nothing for anyone
            var raw = sessionStorage.getItem('gr-lib:map');
        // whose tab had already read the old one - the exact failure you hit
            var o = raw && JSON.parse(raw);
        // while setting this up. mapPromise already collapses it to one
            if (o && (Date.now() - o.t) < TTL) cached = o.v;
        // request per page load, and the browser's own HTTP cache handles
        } catch (e) { }
        // the rest; a stale map is far more expensive than a small fetch.
        if (cached) { mapPromise = Promise.resolve(cached); return mapPromise; }
         try { sessionStorage.removeItem('gr-lib:map'); } catch (e) { }


         var url = (window.mw && mw.util && mw.util.getUrl)
         var url = (window.mw && mw.util && mw.util.getUrl)
Line 3,217: Line 3,297:
             .then(function (r) { return r.ok ? r.json() : {}; })
             .then(function (r) { return r.ok ? r.json() : {}; })
             .then(function (m) {
             .then(function (m) {
                 // Either a flat {page: id} object, or {pages:{...},
                 // Either a flat {page: id} object, or {pages, works,
                 // locators:{...}} when division names are recorded too.
                 // locators} when names and division names are recorded too.
                 m = m || {};
                 m = m || {};
                 if (!m.pages) m = { pages: m, locators: {} };
                 if (!m.pages) m = { pages: m };
                m.works = m.works || {};
                 m.locators = m.locators || {};
                 m.locators = m.locators || {};
                 try { sessionStorage.setItem('gr-lib:map', JSON.stringify({ t: Date.now(), v: m })); }
                 try {
                 catch (e) { }
                    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;
                 return m;
             })
             })
Line 3,263: Line 3,349:
             return lookup(gid).then(function (g) {
             return lookup(gid).then(function (g) {
                 if (!g) return;
                 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.name)      t.setAttribute('data-doc-title', g.name);
                 if (g.nameIAST)  t.setAttribute('data-doc-title-iast', g.nameIAST);
                 if (g.nameIAST)  t.setAttribute('data-doc-title-iast', g.nameIAST);
Line 3,278: Line 3,373:
     var cataloguePromise = null;
     var cataloguePromise = null;
     function catalogue() {
     function catalogue() {
        if (libraryDown) return Promise.resolve({});
         if (cataloguePromise) return cataloguePromise;
         if (cataloguePromise) return cataloguePromise;
         cataloguePromise = fetch(API + '?limit=1000', {
         cataloguePromise = fetch(API + '?limit=1000', {
             credentials: 'omit', headers: { 'Accept': 'application/json' }
             credentials: 'omit', headers: { 'Accept': 'application/json' }
         }).then(function (r) { return r.ok ? r.json() : null; })
         }).then(function (r) {
              if (!r.ok) { libraryDown = true; return null; }
              return r.json();
          })
           .then(function (j) {
           .then(function (j) {
               var list = (j && (j.granthas || j.data || j.results)) || [];
               var list = (j && (j.granthas || j.data || j.results)) || [];
Line 3,294: Line 3,393:
         var want = [], seen = {};
         var want = [], seen = {};
         (ids || []).forEach(function (i) { if (i && !seen[i]) { seen[i] = 1; want.push(i); } });
         (ids || []).forEach(function (i) { if (i && !seen[i]) { seen[i] = 1; want.push(i); } });
         var missing = want.filter(function (i) { return !cacheGet(i); });
         // lookup() answers from the map page without a network call, so
         if (missing.length > 5) {
         // resolving fifty works costs one shared map fetch, not fifty
            return catalogue().then(function (by) {
        // requests. Only ids the map does not name reach the library.
                var out = {};
                want.forEach(function (i) { out[i] = by[i] || cacheGet(i) || null; });
                return out;
            });
        }
         return Promise.all(want.map(lookup)).then(function (vals) {
         return Promise.all(want.map(lookup)).then(function (vals) {
             var out = {};
             var out = {};
Line 3,307: Line 3,401:
             return out;
             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;
     }
     }


Line 3,348: Line 3,458:
     window.grLibraryVersion = '2026-09-30a';
     window.grLibraryVersion = '2026-09-30a';
     window.grLibrary = { lookup: lookup, lookupMany: lookupMany, catalogue: catalogue,
     window.grLibrary = { lookup: lookup, lookupMany: lookupMany, catalogue: catalogue,
                        teekaLabel: teekaLabel,
                        labelFor: function (slug) { return labelBySlug[slug] || ''; },
                         idForPage: idForPage, map: granthaMap, api: API,
                         idForPage: idForPage, map: granthaMap, api: API,
                         relabelDocLinks: relabelDocLinks };
                         relabelDocLinks: relabelDocLinks };