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>', | ||
'</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 | var here = (window.mw && mw.config.get('wgPageName')) || ''; | ||
var | var frag = anchorId ? '#' + encodeURIComponent(anchorId) : ''; | ||
var base; | |||
return | 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: | ||
} | } | ||
// | // APA 7th, webpage on a website (§10.16): | ||
// reference | // 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 | var retrieved = retrievedToday(); | ||
if (author) | var loc = locatorText(anchor, src.scheme); | ||
var head, headHtml, showSite; | |||
if (author) { | |||
head = author + '. (n.d.). ' + title + '.'; | |||
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- | // 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 | ||
+ ' | // 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; | ||
var cite = buildCitation(rec); | |||
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. | ||
// The count badge is gone. It sat on the round toggle, which clips its | |||
// own overflow to stay circular, so the badge was cut off on every side | |||
// and told the reader nothing they could act on. Kept as a no-op so the | |||
// five call sites that report a change still read clearly. | |||
function updateBadge() { } | |||
}() ); | }() ); | ||