MediaWiki:Gadget-GrAnnotations.js: Difference between revisions

No edit summary
No edit summary
Line 836: Line 836:
   // Walks up from the cited block: an interleaved commentary block states
   // Walks up from the cited block: an interleaved commentary block states
   // its own work, otherwise the page's title element does.
   // its own work, otherwise the page's title element does.
  // A reference written in English cites the work in IAST, so prefer the
  // transliterated forms the library supplies and keep Devanagari as the
  // fallback. common.js writes the library's values onto .gr-doc-title as
  // soon as they arrive, so reading the DOM here always gets the current
  // names without this module knowing the library exists.
   function citeSourceFor(el) {
   function citeSourceFor(el) {
     var n = el;
     var n = el;
Line 841: Line 846:
       if (n.getAttribute && n.getAttribute('data-cite-title')) {
       if (n.getAttribute && n.getAttribute('data-cite-title')) {
         return {
         return {
           author: n.getAttribute('data-cite-author') || '',
           author: n.getAttribute('data-cite-author-iast') ||
           title:  n.getAttribute('data-cite-title') || '',
                  n.getAttribute('data-cite-author') || '',
           title:  n.getAttribute('data-cite-title-iast') ||
                  n.getAttribute('data-cite-title') || '',
           page:  n.getAttribute('data-cite-page') || ''
           page:  n.getAttribute('data-cite-page') || ''
         };
         };
Line 850: Line 857:
     var t = document.querySelector('.gr-doc-title');
     var t = document.querySelector('.gr-doc-title');
     return {
     return {
       author: (t && t.getAttribute('data-author')) || '',
       author: (t && (t.getAttribute('data-author-iast') ||
       title:  (t && (t.getAttribute('data-doc-title') || t.textContent.trim())) || document.title,
                    t.getAttribute('data-author'))) || '',
       page:  (window.mw && mw.config.get('wgPageName')) || ''
       title:  (t && (t.getAttribute('data-doc-title-iast') ||
                    t.getAttribute('data-doc-title') ||
                    t.textContent.trim())) || document.title,
       page:  (window.mw && mw.config.get('wgPageName')) || '',
      scheme: (t && t.getAttribute('data-locator-scheme')) || ''
     };
     };
   }
   }


  // Any block carrying a library id gets its author/title filled in from
  // the library, whether common.js injected it or the page was built with
  // it already in place. Doing it here rather than only at injection time
  // means a citation never depends on which path put the block on screen.
  function hydrateCitationSources(root) {
    if (!window.grLibrary) return;
    var nodes = (root || document).querySelectorAll('[data-cite-grantha-id]');
    Array.prototype.forEach.call(nodes, function (el) {
      if (el.getAttribute('data-cite-hydrated')) return;
      var gid = el.getAttribute('data-cite-grantha-id');
      if (!gid) return;
      el.setAttribute('data-cite-hydrated', '1');
      window.grLibrary.lookup(gid).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);
      });
    });
  }
  // ── LOCATOR ─────────────────────────────────────────────────────────
  // "KKN_C01_S01_V03" is a build artefact, not a reference. A reader
  // citing it wants 1.1.3, and ideally to be told that those numbers are
  // adhyaya, valli and mantra. The page declares the division names it
  // knows (data-locator-scheme); where it declares none we still give the
  // dotted form, which is correct for every text - it is only the NAMES
  // of the divisions that vary from work to work.
  function parseScheme(s) {
    var out = {};
    (s || '').split(',').forEach(function (pair) {
      var i = pair.indexOf('=');
      if (i > 0) out[pair.slice(0, i).trim()] = pair.slice(i + 1).trim();
    });
    return out;
  }
  // Segments that qualify the passage rather than number it: _B01 is the
  // first paragraph of the bhashya ON that verse, and a bare letter with
  // no number (Katha's "_I_" for an introduction) marks a position, not a
  // division. A NUMBERED segment is always part of the path - in
  // RG_C01_I01_V01 the I is a sukta, and reading it as Katha's
  // introduction marker turned 1.1.1 into "1.1, Sukta 1".
  function isQualifier(k, n) { return k === 'B' || !n; }
  function parseLocator(id) {
    if (!id) return null;
    var segs = String(id).split('_');
    var path = [], qual = [];
    for (var i = 1; i < segs.length; i++) {
      var m = /^([A-Za-z]+)(\d*)$/.exec(segs[i]);
      if (!m) continue;
      var k = m[1].toUpperCase(), n = m[2] ? String(parseInt(m[2], 10)) : '';
      (isQualifier(k, n) ? qual : path).push({ k: k, n: n });
    }
    if (!path.length && !qual.length) return null;
    return { path: path, qual: qual };
  }
  function locatorText(id, scheme) {
    var p = parseLocator(id);
    if (!p) return { numeric: id || '', spelled: '' };
    var labels = parseScheme(scheme);
    var numeric = p.path.map(function (s) { return s.n; }).filter(Boolean).join('.');
    var spelledParts = p.path.map(function (s) {
      return labels[s.k] ? labels[s.k] + ' ' + s.n : null;
    });
    var spelled = spelledParts.indexOf(null) === -1 ? spelledParts.join(', ') : '';
    p.qual.forEach(function (s) {
      var lab = labels[s.k] ||
                (s.k === 'B' ? 'bhāṣya' : (s.k === 'I' ? 'intro' : s.k));
      var bit = lab + (s.n ? ' ' + s.n : '');
      numeric += (numeric ? ', ' : '') + bit;
      if (spelled) spelled += ', ' + bit;
    });
    return { numeric: numeric || id, spelled: spelled };
  }
  // Version stamp, so it is possible to tell from the browser console
  // which build of this gadget a page is actually running. Check with
  //  window.grAnnotationsVersion
  window.grAnnotationsVersion = '2026-09-30a';
  // A citation is worthless if its link doesn't open, so prefer URLs we
  // KNOW are good over ones we assemble.
  //  - Same page as the reader: use the address they're already at. A wiki
  //    whose served paths differ from wgArticlePath (or that sits behind a
  //    rewrite) would otherwise yield a link that 404s.
  //  - Another page (an injected teeka block cites its own subpage): use
  //    mw.util.getUrl, the same builder every working link on the wiki
  //    comes from. wgArticlePath is only the last resort.
   function citeUrl(pageName, anchorId) {
   function citeUrl(pageName, anchorId) {
     var path = (window.mw && mw.config.get('wgArticlePath')) || '/wiki/$1';
     var here = (window.mw && mw.config.get('wgPageName')) || '';
     var page = pageName || (window.mw && mw.config.get('wgPageName')) || '';
     var frag = anchorId ? '#' + encodeURIComponent(anchorId) : '';
    var base = path.replace('$1', String(page).split('/').map(encodeURIComponent).join('/'));
    var base;
     return location.origin + base + (anchorId ? '#' + encodeURIComponent(anchorId) : '');
    if (!pageName || String(pageName) === String(here)) {
      base = location.origin + location.pathname;
    } else if (window.mw && mw.util && mw.util.getUrl) {
      base = location.origin + mw.util.getUrl(String(pageName));
    } else {
      var path = (window.mw && mw.config.get('wgArticlePath')) || '/wiki/$1';
      base = location.origin +
            path.replace('$1', String(pageName).split('/').map(encodeURIComponent).join('/'));
    }
     return base + frag;
   }
   }


Line 875: Line 987:
   }
   }


   // rec: the bookmark record. Returns {text, url} - text is the full APA
   // APA 7th, webpage on a website (§10.16):
   // reference with the link in it, ready to paste anywhere.
  //    Author. (n.d.). Title of work. Site Name. Retrieved <date>, from <URL>
  // Two rules this has to honour and an earlier draft did not:
  //  1. No known author -> the TITLE moves into the author position; you
  //      never leave a bare "(n.d.)" with nothing in front of it. And when
  //      the title occupies that slot the site name is not repeated after it.
  //  2. A locator (our block id) does NOT belong in the reference entry.
  //      The parenthetical slot after a title is for edition/format
  //      descriptors, not section numbers. APA puts locators in the in-text
  //      citation, so the block id goes there: (Author, n.d., IS_C01_I03).
  // The retrieval date is correct here because the page is designed to
   // change and we link to the live version rather than a permalink.
  // Titles of standalone works are italicised, which only the HTML flavour
  // of the clipboard can carry.
   function buildCitation(rec) {
   function buildCitation(rec) {
     var src = rec.citeSource || {};
     var src   = rec.citeSource || {};
     var author = src.author || '';
     var author = (src.author || '').trim();
     var title  = src.title || '';
     var title  = (src.title || '').trim();
     var anchor = rec.anchor || '';
     var anchor = rec.anchor || '';
    var site  = (siteName() || '').trim();
     var url    = citeUrl(src.page, anchor);
     var url    = citeUrl(src.page, anchor);
     var parts  = [];
     var retrieved = retrievedToday();
     if (author) parts.push(author + '.');
    var loc    = locatorText(anchor, src.scheme);
     parts.push('(n.d.).');
 
    parts.push(title + (anchor ? ' (' + anchor + ')' : '') + '.');
    var head, headHtml, showSite;
     parts.push(siteName() + '.');
     if (author) {
    parts.push('Retrieved ' + retrievedToday() + ', from ' + url);
      head = author + '. (n.d.). ' + title + '.';
     return { text: parts.join(' '), url: url };
      headHtml = esc(author) + '. (n.d.). <i>' + esc(title) + '</i>.';
      showSite = site && site !== author;
     } else {
      // Title in the author position - italicised there too, and the site
      // name is dropped when it would only repeat what the title says.
      head = title + '. (n.d.).';
      headHtml = '<i>' + esc(title) + '</i>. (n.d.).';
      showSite = site && site !== title;
    }
 
    var tail = ' Retrieved ' + retrieved + ', from ' + url;
    var reference = head + (showSite ? ' ' + site + '.' : '') + tail;
     var referenceHtml = headHtml + (showSite ? ' ' + esc(site) + '.' : '')
        + ' Retrieved ' + esc(retrieved) + ', from '
        + '<a href="' + esc(url) + '">' + esc(url) + '</a>';
 
     // The in-text citation carries the locator, expanded: 1.1.3, not
    // KKN_C01_S01_V03. The name is shortened the way an in-text citation
    // shortens one - the library's parenthetical alias ("Śrīman
    // Madhvācārya (Ānandatīrtha)") belongs in the reference, not here.
    var shortName = (author || title).replace(/\s*\([^)]*\)\s*$/, '').trim();
    var inText = '(' + (shortName || author || title) + ', n.d.'
              + (loc.numeric ? ', ' + loc.numeric : '') + ')';
 
    return { reference: reference, referenceHtml: referenceHtml,
            inText: inText, url: url,
            locator: loc.numeric, locatorSpelled: loc.spelled };
   }
   }


   function copyToClipboard(text, done) {
  // html is optional. When given, both flavours go on the clipboard so a
  // paste into Word/Docs keeps the italicised title and the live link,
  // while a paste into a plain field (or the address bar) gets the text.
   function copyToClipboard(text, done, html) {
    if (html && window.ClipboardItem && navigator.clipboard && navigator.clipboard.write) {
      try {
        var item = new ClipboardItem({
          'text/html':  new Blob([html], { type: 'text/html' }),
          'text/plain': new Blob([text], { type: 'text/plain' })
        });
        navigator.clipboard.write([item]).then(done).catch(function () {
          plainCopy(text, done);
        });
        return;
      } catch (e) { /* fall through */ }
    }
    plainCopy(text, done);
  }
  function plainCopy(text, done) {
     if (navigator.clipboard && navigator.clipboard.writeText) {
     if (navigator.clipboard && navigator.clipboard.writeText) {
       navigator.clipboard.writeText(text).then(done).catch(function () { legacyCopy(text, done); });
       navigator.clipboard.writeText(text).then(done).catch(function () { legacyCopy(text, done); });
Line 1,030: Line 1,199:
             + '<div class="gra-bookmark-name">'+esc(b.name)+'</div>'
             + '<div class="gra-bookmark-name">'+esc(b.name)+'</div>'
             + (b.quote ? '<div class="gra-bookmark-quote">'+esc(b.quote)+'</div>' : '')
             + (b.quote ? '<div class="gra-bookmark-quote">'+esc(b.quote)+'</div>' : '')
             + '<div class="gra-bookmark-cite">'+esc(cite.text)+'</div>'
             + '<div class="gra-bookmark-cite">'+cite.referenceHtml+'</div>'
             + '<button type="button" class="gra-cite-copy" data-cite-id="'+esc(b.id)+'">'
            + '<div class="gra-bookmark-intext">In text: '+esc(cite.inText)
             + 'Copy citation</button>'
            + (cite.locatorSpelled ? ' <span class="gra-cite-locator">('
                + esc(cite.locatorSpelled) + ')</span>' : '')
            + '</div>'
            + '<div class="gra-cite-actions">'
             + '<button type="button" class="gra-cite-copy" data-cite-what="reference"'
            + ' data-cite-id="'+esc(b.id)+'">Copy reference</button>'
             + '<button type="button" class="gra-cite-copy" data-cite-what="inText"'
            + ' data-cite-id="'+esc(b.id)+'">Copy in-text</button>'
            + '<button type="button" class="gra-cite-copy" data-cite-what="url"'
            + ' data-cite-id="'+esc(b.id)+'">Copy link</button>'
            + '</div>'
             + '</div>'
             + '</div>'
             + '<button class="gra-bookmark-del" data-del-id="'+esc(b.id)+'" title="Remove">×</button>'
             + '<button class="gra-bookmark-del" data-del-id="'+esc(b.id)+'" title="Remove">×</button>'
Line 1,336: Line 1,515:
       var rec = _bookmarks.filter(function(b){ return b.id === id; })[0];
       var rec = _bookmarks.filter(function(b){ return b.id === id; })[0];
       if (!rec) return;
       if (!rec) return;
       copyToClipboard(buildCitation(rec).text, function(){
       var cite = buildCitation(rec);
        var was = $btn.text();
      var what = $btn.attr('data-cite-what') || 'reference';
      // Only the reference carries formatting worth preserving; the in-text
      // form and the bare link must stay plain so they survive a paste into
      // the address bar.
      var text = cite[what] || cite.reference;
      var html = (what === 'reference') ? cite.referenceHtml : null;
      var was = $btn.text();
      copyToClipboard(text, function(){
         $btn.text('Copied').addClass('gra-cite-copy--done');
         $btn.text('Copied').addClass('gra-cite-copy--done');
         setTimeout(function(){ $btn.text(was).removeClass('gra-cite-copy--done'); }, 1600);
         setTimeout(function(){ $btn.text(was).removeClass('gra-cite-copy--done'); }, 1600);
       });
       }, html);
     });
     });
     $paneBookmarks.on('click', '.gra-bookmark-del', function(e){
     $paneBookmarks.on('click', '.gra-bookmark-del', function(e){
Line 1,501: Line 1,687:
     buildDom();
     buildDom();
     wireEvents();
     wireEvents();
    hydrateCitationSources();
    // Commentary blocks arrive after the page does, so hydrate again when
    // they land rather than only once at boot.
    window.addEventListener('gr-new-content', function (e) {
      hydrateCitationSources(e && e.detail && e.detail.container);
    });


     function paint() {
     function paint() {