MediaWiki:Gadget-GrAnnotations.js: Difference between revisions

No edit summary
No edit summary
 
(3 intermediate revisions by the same user not shown)
Line 588: Line 588:
       '<button id="gra-toggle" aria-label="Notes">',
       '<button id="gra-toggle" aria-label="Notes">',
       '  <span class="gra-icon gra-icon-note" id="gra-toggle-icon" aria-hidden="true"></span>',
       '  <span class="gra-icon gra-icon-note" id="gra-toggle-icon" aria-hidden="true"></span>',
      '  <span id="gra-toggle-badge" aria-live="polite"></span>',
       '</button>',
       '</button>',
     ].join('') );
     ].join('') );
Line 836: Line 835:
   // 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 845:
       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 856:
     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
  // APA 7 asks for a retrieval date only when the content is designed to
  // change and is not archived. These are settled texts with a fixed
  // structure, so the date is omitted. Set true to restore it.
  var SHOW_RETRIEVAL_DATE = false;
  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 991:
   }
   }


   // 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.
  // The library records names with their honorifics and, often, a
   // parenthetical alias: "Śrīman Madhvācārya (Ānandatīrtha)". A reference
  // entry carries the name a reader would look up, so the honorific and
  // the alias come off - leaving "Madhvācārya". Only applied to the
  // romanised form; the Devanagari is left exactly as the library has it,
  // because there the honorific is fused to the name by sandhi
  // (श्रीमत् + आनन्दतीर्थः -> श्रीमदानन्दतीर्थः) and cannot be sliced off safely.
  var HONORIFIC_RE = /^(?:\u015br\u012b|\u015br\u012bman|\u015br\u012bmat|\u015br\u012bmad|sri|shri|sree)\s+/i;
  function citationAuthor(name) {
    var n = (name || '').trim();
    if (!/[A-Za-z]/.test(n)) return n;            // Devanagari - leave alone
    n = n.replace(/\s*\([^)]*\)\s*$/, '').trim();
    for (var i = 0; i < 3 && HONORIFIC_RE.test(n); i++) {
      n = n.replace(HONORIFIC_RE, '').trim();
    }
    return n || (name || '').trim();
  }
 
   function buildCitation(rec) {
   function buildCitation(rec) {
     var src = rec.citeSource || {};
     var src   = rec.citeSource || {};
     var author = src.author || '';
     var author = citationAuthor(src.author);
     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;
    }
 
    // APA 7 (section 9.16) asks for a retrieval date only when the content
    // is designed to change and is not archived. A wiki page can change, so
    // the date is defensible - but these are settled texts, and a reader may
    // reasonably prefer the shorter form. Set SHOW_RETRIEVAL_DATE to false
     // for: Author. (n.d.). Title. Site. URL
    var tail = SHOW_RETRIEVAL_DATE
        ? ' Retrieved ' + retrieved + ', from ' + url
        : ' ' + url;
    var reference = head + (showSite ? ' ' + site + '.' : '') + tail;
     var referenceHtml = headHtml + (showSite ? ' ' + esc(site) + '.' : '') + ' '
        + (SHOW_RETRIEVAL_DATE ? '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,228:
             + '<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>'
            // One button. The reference already carries the work, the
             + '<button type="button" class="gra-cite-copy" data-cite-id="'+esc(b.id)+'">'
            // locator's page and the link, so a separate "copy link" only
             + 'Copy citation</button>'
            // asked the reader to decide something they don't need to.
            // The in-text form stays on the card because it is what goes
            // in a sentence, but it copies on click rather than earning a
            // button of its own.
            // Where in the text this sits, spelled out - "Adhyaya 3,
            // Mantra 4" rather than MUN_C03_V04. Not part of the citation
            // (APA keeps a locator out of the reference entry), so it is
            // set apart as a caption and is not included in the copy.
             + (cite.locatorSpelled || cite.locator
                ? '<div class="gra-bookmark-locator">'
                    + esc(cite.locatorSpelled || cite.locator) + '</div>'
                : '')
            + '<div class="gra-bookmark-cite">'+cite.referenceHtml+'</div>'
            + '<div class="gra-cite-actions">'
             + '<button type="button" class="gra-cite-copy" data-cite-what="reference"'
            + ' data-cite-id="'+esc(b.id)+'">Copy citation</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,550:
       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,722:
     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() {
Line 1,523: Line 1,750:
   // the panel without opening it — worth having now that notes can arrive
   // the panel without opening it — worth having now that notes can arrive
   // from another device.
   // from another device.
   function updateBadge() {
   // The count badge is gone. It sat on the round toggle, which clips its
    var n = _notes.length + _bookmarks.length;
  // own overflow to stay circular, so the badge was cut off on every side
    var $b = $('#gra-toggle-badge');
  // and told the reader nothing they could act on. Kept as a no-op so the
    if (!$b.length) return;
  // five call sites that report a change still read clearly.
    $b.text(n > 99 ? '99+' : String(n)).css('display', n ? 'block' : 'none');
  function updateBadge() { }
  }


}() );
}() );