// ==UserScript== // @name Radiopaedia Cite // @namespace https://radiopaedia.work/ // @homepageURL https://github.com/gmadevs/radiopaedia-citation-manager // @supportURL https://github.com/gmadevs/radiopaedia-citation-manager/issues // @downloadURL https://raw.githubusercontent.com/gmadevs/radiopaedia-citation-manager/main/radiopaedia-cite.user.js // @updateURL https://raw.githubusercontent.com/gmadevs/radiopaedia-citation-manager/main/radiopaedia-cite.user.js // @license MIT // @version 1.5.7 // @description A citation picker in the article editor's own toolbar, beside H3, and a characters grid next to it. Press it and type: the references this article already has, filtered as you write, and one press puts the number in the text where the caret was — merged into the marker beside it when there is one, 2,3 and 2-4 the way Radiopaedia writes them. Paste an identifier it has not got yet - a DOI, a PMID, a PMCID, a PII, an ISBN, a Google Books id, or a URL to the paper - and it is looked up on radiopaedia.work/cite, added as the next numbered reference, and cited in the same press. // @match https://radiopaedia.org/* // @connect radiopaedia.work // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant unsafeWindow // @run-at document-idle // @noframes // ==/UserScript== /* * How this hangs together * ----------------------- * Citing a paper on Radiopaedia is two things that live at opposite ends of * the edit page and have to agree with each other: a `` marker in the * prose, and a numbered line in the reference list at the bottom. The number * is the only thing joining them. Nothing on the page checks that it is the * right one, and by the twentieth reference nobody remembers which paper is * eleven — so the marker gets typed from memory, or by scrolling down, losing * the caret, scrolling back up and hoping. * * This is the other way round: the caret stays where it is, and the list comes * to it. One button in the editor's own toolbar, beside H3; one panel; you * type; you press return. * * Three things can be asked of it, and they are the three things a person * actually wants: * * - cite one of the references already down there. Type any word of it — * author, journal, year, the number itself — and the list narrows the way * Zotero's does. Return puts the marker in. * - cite the last one you added, which is the commonest of all: you have * just written a paragraph out of the paper you added a minute ago. It is * the row the panel opens on, so that press is open-and-return and nothing * else. * - cite a paper that is NOT down there yet. Paste anything the citation * tool can resolve — a DOI, a PMID, a PMCID, a PII, an ISBN, a Google * Books volume id, or a URL to the paper, to a Wikipedia page, to any * website — and it is looked up on radiopaedia.work/cite, shown to you in * full, and on your say-so added as the next numbered reference AND cited * in the text, in one press. * * Which of those it is decides where the offer stands in the list, and * that is not a cosmetic decision: an identifier is unambiguous, so it * goes first and return looks it up. Words are not — "ependymoma" is * overwhelmingly "cite the ependymoma paper I already have" — so a plain * search goes last, under the references it might have meant. * * Where the number goes, and in what shape * ---------------------------------------- * A marker is `1` with a space in front of it — `haemangioblastoma) * 1.` — and it sits INSIDE the sentence, before the full stop. Both * of those are house style and both are easy to get wrong by hand, so neither * is left to the hand: the space is added when the character before is not one * already, and a caret parked immediately after a sentence's full stop hops * back over it rather than dropping the marker outside the sentence. * * When the caret is already beside a marker, the number joins it instead of * standing a second `` next to the first: `2` and 3 becomes * `2,3`, and three or more in a row close up into a range — * `2-4`. Written back out from the numbers, so the list also comes * back sorted and deduplicated, which is the other thing hand-typed markers * get wrong. * * What it writes, and what it does not * ------------------------------------ * The marker is asked for the way a person would ask for it — the editor's own * insert where there is one, otherwise the number typed and then ⌘. (ctrl-. * away from a Mac) pressed on it. Not for tidiness: an editor that keeps its * own model of the document renders the page from that model, and a `` * that never went through it is gone at the next render, leaving the number in * the running text at full size. Typing survives; markup written behind the * editor's back does not. `raiseHere` has the order and the reasons. * * Two places, both of them yours to undo: the `` in the editor, and a new * box in the reference list with `N. …` in it. Nothing is saved — the form is * still sitting there unsubmitted, and every marker can be selected and * deleted like any other text. The fetched citation also goes to the clipboard * on its way past, so a lookup is never lost even if the box could not be * created. * * The reference itself is never rewritten. If what is already down there * differs from what the databases say, that is a job for the citation linter * (radiopaedia-lint's `Lint citation` chip) and not for a tool whose business * is the number. * * What it costs * ------------- * One request, to radiopaedia.work/cite, per lookup you confirm — and a lookup * only ever happens because you typed an identifier and pressed return. * Reading the references costs nothing: they are in the form. The answer is * kept for the tab, so pasting the same PMID twice asks once. */ (function () { 'use strict'; /* Two or more consecutive numbers can be written as a range, and how many * it takes is a house rule rather than a law: Radiopaedia writes 2,3 for a * pair and 2-4 from three up. Set this to 2 and a pair closes up as well. */ const RANGE_FROM = 3; /* A caret sitting immediately after a full stop is a caret that meant to be * just inside the sentence — that is where the marker belongs. Set to false * to put the marker exactly where the caret is and nowhere else. */ const HOP_PUNCTUATION = true; /* The citation worker: give it a PMID, a DOI, an ISBN, a URL or a reference, * and it works out for itself what to look up and which of Crossref, PubMed, * Google Books or Elsevier to ask. Same host and same endpoint the linter's * `Lint citation` chip uses, so a citation added here is already in the * shape that chip will agree with. */ const CITE_URL = 'https://radiopaedia.work/cite?search='; const CITE_TIMEOUT = 60_000; // somebody else's database is at the far end of it const CITE_MAX = 1024 * 1024; // a rendered page; anything bigger is not one const CITE_KEY = 'rcx-cite:'; // one answer, for this tab's session const BOX_TIMEOUT = 6_000; // how long a new reference box gets to appear const CONTEXT_CHARS = 46; // how much of the sentence the panel shows back // What a Cloudflare interstitial carries instead of the answer. const CHALLENGE = ['start_challenge', 'bot_management', 'Verifying you are human']; // ————————————————————————————————————————————————————————————— text /* One line of it, whatever came in. */ function tidy(v) { return String(v ?? '').replace(/\s+/g, ' ').trim(); } /* Typography folded onto its plain forms, and the invisible characters taken * out altogether. `\s` in JavaScript does not cover the zero-width space and * Radiopaedia's text is full of them; one of those inside a `` is * enough for a perfectly good marker to read as something that is not a * marker at all. */ function fold(s) { return String(s ?? '') .replace(/[‘’ʼ]/g, "'") .replace(/[“”]/g, '"') .replace(/[–—−]/g, '-') .replace(/[\u200b-\u200d\u2060\ufeff\u00ad]/g, '') .replace(/\s+/g, ' ') .trim(); } /* The text of something that arrived as markup. A reference is stored with * its `` tags spelled out, and what a person reads in a list of them is * the words. `DOMParser` rather than an `innerHTML` on a detached node: the * document it builds is inert, so nothing in there runs, loads or fetches. * Strings with no `<` in them skip it, which is most of them. */ function plain(html) { const raw = String(html ?? ''); if (!raw.includes('<')) return fold(raw); return fold(new DOMParser().parseFromString(raw, 'text/html').body.textContent); } // Not a checksum: a short, stable key for a long string. function shortHash(text) { let h = 2166136261; for (let i = 0; i < text.length; i++) { h ^= text.charCodeAt(i); h = Math.imul(h, 16777619); } return (h >>> 0).toString(36); } const ordinal = (n) => { const tens = n % 100, ones = n % 10; const suffix = tens >= 11 && tens <= 13 ? 'th' : ones === 1 ? 'st' : ones === 2 ? 'nd' : ones === 3 ? 'rd' : 'th'; return `${n}${suffix}`; }; /* Is this the edit form? * * The path says so on every edit page there is — but the path is a * convention and the form is a fact, so when it does not match, the * reference boxes are asked instead. "Format citation" appears under every * one of them and nowhere else on the site. */ function inEditor() { if (/\/edit(?:\/|$)/.test(location.pathname)) return true; for (const el of document.querySelectorAll('a, button')) { if (tidy(el.textContent).toLowerCase() === FORMAT_LINK) return true; } return false; } // ——————————————————————————————————————————————————————— the numbers /* What a marker says, as numbers. * * `2,4,6`, `2-4`, `1` — and `null` for * anything else, which is the answer that matters. A `` is not * necessarily a citation: articles use it for units and for exponents, and * merging a new reference number into `cm3` would be a strange * way to lose an article. Only a superscript made of digits, commas and * hyphens is one of ours, and even then a backwards or absurd range * ("3-1", "1-400") is read as arithmetic rather than as a citation. */ function markerNumbers(text) { const s = fold(text).replace(/\s+/g, ''); if (!s || !/^[\d,-]+$/.test(s)) return null; const out = []; for (const part of s.split(',')) { const span = /^(\d{1,3})-(\d{1,3})$/.exec(part); if (span) { const from = +span[1], to = +span[2]; if (to <= from || to - from > 60) return null; for (let n = from; n <= to; n++) out.push(n); continue; } if (!/^\d{1,3}$/.test(part)) return null; out.push(+part); } return out.length ? out : null; } /* And back the other way: the numbers, sorted, deduplicated, with runs of * consecutive ones closed up into ranges. This is the only place a marker's * text is ever written, which is why a marker this script has touched is * always in order even when what it merged into was not. */ function markerText(numbers) { const nums = [...new Set(numbers)].filter((n) => Number.isInteger(n) && n > 0) .sort((a, b) => a - b); const parts = []; for (let i = 0; i < nums.length;) { let j = i; while (j + 1 < nums.length && nums[j + 1] === nums[j] + 1) j++; if (j - i + 1 >= RANGE_FROM) { parts.push(`${nums[i]}-${nums[j]}`); i = j + 1; } else { parts.push(String(nums[i])); i++; } } return parts.join(','); } // —————————————————————————————————————————————————— the reference list /* A reference, on the edit page, is a `