Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <nowiki>
const editableReferences = [];
let currentlySelectedRef;
const rfApi = new mw.Api();
let refsSaved = 0;
const referenceTemplateData = {
"none": [
"wikitext"
],
"web": [
"url",
"title",
"authors",
"date",
"website",
"accessdate",
"publisher",
"archiveurl"
],
"news": [
"url",
"title",
"authors",
"date",
"work",
"accessdate",
"publisher",
"archiveurl"
]
};
const supportedArgs = [
"url", "title", "archive-date", "archivedate", "website", "work",
"publisher", "archiveurl", "archive-url", "date", "url-status"
];
async function runReferenceEditor() {
const page = await rfApi.get({
action: 'query',
prop: 'revisions',
rvprop: 'content',
titles: mw.config.get('wgPageName'),
formatversion: 2,
rvslots: '*'
});
const wikitext = page.query.pages[0].revisions[0].slots.main.content;
const references = [...wikitext.matchAll(/<ref(?: name="?([^\/]+?)"?)?>(.+?)<\/ref>/gmsi)];
const referenceArgs = references
.map(ref => [...ref[2].matchAll(/\|(?:\s+)?([^=]+?)(?:\s+)?=(?:\s+)?([^\|]+?)(\s+?)?(?=[\|]|(?:}}$))/gmsi)])
.map(ref => ref.map(a => [a[1].toLowerCase(), a[2]]));
const cleanedRefs = [];
for (let i = 0; i < references.length; i++) {
const refUrl = references[i][2].match(/https?:\/\/.+?(?=[\| }])/);
const citeType = references[i][2].match(/{{cite (.+?)(\s+)?(\||})/i);
if (!refUrl) {
continue;
}
cleanedRefs.push({
type: citeType ? citeType[1] : null,
url: refUrl[0],
args: referenceArgs[i],
wikitext: references[i][2]
});
}
const refElems = [...document.querySelectorAll("ol.references > li")];
for (let refElem of refElems) {
const links = [...refElem.querySelectorAll("a")];
for (let item of cleanedRefs) {
for (let link of links) {
if (link.href === item.url && (item.type in referenceTemplateData || !item.type)) {
editableReferences.push({ item, refElem });
refElem.style.position = "relative";
refElem.innerHTML += `<div class="referenceEditorButton" style="position: absolute; top: -3px; left: -42px; cursor: pointer;" onclick="editReference(${editableReferences.length - 1})"><img style="width: 16px; height: 16px;" draggable="false" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANlJREFUWEftl8EOhCAMROX/P1oDSUkpRaAdOCgeNntQ3qOUQcO197oZLsT/6WfTxeGEDLsENHiS+I1AnKxahZUVICBnSIncA801GmhQbRIViI1TiMWHPXCtj1rjqdWWAt4l6U2mGh8p0IOruw4lYIKTURWPA43HbzHDEQIuuFfADfcIQOBWARh8lYDc61ok5ya2bMO3CvRiGRJEU1Erot4tMAuXxzBUYPTceA06Sw9MBmVx2rorMAuHL8EROBX4fgXQX0vdty0KhpEj1lJ+/kzztZxuWinRjO0HcnI/HmlWsQEAAAAASUVORK5CYIIA"></div>`;
}
}
}
}
}
function editReference(number) {
[...document.querySelectorAll(".referenceEditor")].forEach(e => e.remove());
currentlySelectedRef = editableReferences[number];
const editorElem = document.createElement("div");
editorElem.className = "referenceEditor";
editorElem.style.width = "600px";
editorElem.style.height = "500px";
editorElem.style.position = "fixed";
editorElem.style.top = "calc(50% - 250px)";
editorElem.style.left = "calc(50% - 300px)";
editorElem.style.background = "white";
editorElem.style.border = "1px solid #333";
editorElem.style.overflowY = "auto";
editorElem.innerHTML = `
<div id="referenceType">
<select name="referenceType">
<option value="none">No reference template</option>
<option value="web">{{cite web}}</option>
<option value="news">{{cite news}}</option>
</select>
</div>
<div id="referenceArgs"></div>
<div id="additionalArgs"></div>
<div id="referenceEditorButtons">
<button onclick="this.parentElement.parentElement.remove()">Cancel</button>
<button onclick="saveButtonClicked(this)">Save</button>
</div>
`;
document.body.appendChild(editorElem);
const selectElem = document.querySelector("select[name=referenceType]");
selectElem.addEventListener("change", event => {
selectReferenceType(event.target.value);
});
selectReferenceType(editableReferences[number].item.type || "none");
selectElem.value = editableReferences[number].item.type || "none";
}
function selectReferenceType(type) {
const argsContainer = document.querySelector("#referenceArgs");
const additionalContainer = document.querySelector("#additionalArgs");
argsContainer.innerHTML = "";
additionalContainer.innerHTML = "";
if (!(type in referenceTemplateData)) {
type = "none";
}
const argDict = {};
for (let item of currentlySelectedRef.item.args) {
argDict[item[0]] = item[1];
}
for (let item of referenceTemplateData[type]) {
switch (item) {
case "wikitext":
argsContainer.innerHTML += `
<div>
<span class="title">Wikitext</span>
<div>
<textarea id="referenceEditorWikitext">${escapeHTML(currentlySelectedRef.item.wikitext)}</textarea>
</div>
</div>
`;
return;
case "title":
argsContainer.innerHTML += `
<div>
<span class="title">Title</span>
<div>
<input id="referenceEditorTitle" class="large" value="${escapeHTML(argDict["title"])}">
</div>
</div>
`;
break;
case "website":
argsContainer.innerHTML += `
<div>
<span class="title">Website</span>
<div>
<input id="referenceEditorWebsite" class="large" value="${escapeHTML(argDict["website"])}">
</div>
</div>
`;
break;
case "work":
argsContainer.innerHTML += `
<div>
<span class="title">Work</span>
<div>
<input id="referenceEditorWork" class="large" value="${escapeHTML(argDict["work"])}">
</div>
</div>
`;
break;
case "date":
const date = new Date(argDict["date"]);
let day = "", month = "", year = "";
if (date.toString() !== "Invalid Date") {
day = date.getUTCDate();
month = date.getUTCMonth() + 1;
year = date.getUTCFullYear();
}
argsContainer.innerHTML += `
<div>
<span class="title">Date</span>
<div>
<input id="referenceEditorDay" type="number" class="small" value="${day}" placeholder="Day">
<input id="referenceEditorMonth" type="number" class="small" value="${month}" placeholder="Month">
<input id="referenceEditorYear" type="number" class="small" value="${year}" placeholder="Year">
</div>
</div>
`;
break;
case "url":
argsContainer.innerHTML += `
<div>
<span class="title">URL</span>
<div>
<input id="referenceEditorURL" class="large" value="${escapeHTML(currentlySelectedRef.item.url)}">
</div>
</div>
`;
break;
case "authors":
let authorCount = "first" in argDict || "first1" in argDict ? 1 : 0;
while ("first" + (authorCount + 1) in argDict) {
authorCount++;
}
let authorsHTML = `
<span class="title">Authors</span><div id="referenceEditorAuthors">
`;
for (let i = 0; i < authorCount; i++) {
authorsHTML += `
<div class="referenceEditorAuthor">
<input placeholder="Last" class="referenceEditorLast" value="${escapeHTML(i === 0 ? argDict["last"] || argDict["last1"] || "" : argDict["last" + (i + 1)] || "")}">
<input placeholder="First" class="referenceEditorFirst" value="${escapeHTML(i === 0 ? argDict["first"] || argDict["first1"] || "" : argDict["first" + (i + 1)] || "")}">
<button onclick="this.parentElement.remove()">Remove</button>
</div>
`;
}
authorsHTML += `
<span style="cursor: pointer; user-select: none; font-size: 0.9em;" onclick="addAdditionalAuthor()">+ Add additional author</span></div>
`;
argsContainer.innerHTML += `<div>${authorsHTML}</div>`;
break;
case "publisher":
if (!("publisher" in argDict)) {
additionalContainer.innerHTML += `
<span onclick="addAdditionalArg('publisher'); this.remove();">+ Publisher</span>
`;
} else {
addAdditionalArg("publisher", { publisher: argDict["publisher"] });
}
break;
case "archiveurl":
if (!("archiveurl" in argDict) && !("archive-url" in argDict)) {
additionalContainer.innerHTML += `
<span onclick="addAdditionalArg('archiveurl'); this.remove();">+ Archive URL</span>
`;
} else {
const date = new Date(argDict["archive-date"] || argDict["archivedate"]);
let day = "", month = "", year = "";
if (date.toString() !== "Invalid Date") {
day = date.getUTCDate();
month = date.getUTCMonth() + 1;
year = date.getUTCFullYear();
}
addAdditionalArg("archiveurl", { day, month, year, url: argDict["archiveurl"] || argDict["archive-url"], status: argDict["url-status"] });
}
break;
default:
break;
}
}
for (let item in argDict) {
if (supportedArgs.includes(item) || item.startsWith("last") || item.startsWith("first")) {
continue;
}
argsContainer.innerHTML += `
<div>
<span class="title">${item}</span>
<div>
<input class="large" data-arg="${item}" value="${escapeHTML(argDict[item])}">
</div>
</div>
`;
}
}
function addAdditionalArg(type, data) {
data = data || {};
switch (type) {
case "archiveurl":
document.querySelector("#referenceArgs").insertAdjacentHTML("beforeend", `
<div>
<span class="title">Archive URL</span>
<div>
<input id="referenceEditorArchiveURL" value="${escapeHTML(data.url || "")}" class="large">
</div>
<div class="referenceEditorArgTools">
<button onclick="referenceFixerLoadArchive(this)">Load</button>
</div>
</div>
<div>
<span class="title">Archive date</span>
<div>
<input id="referenceEditorArchiveDay" type="number" class="small" value="${data.day || ""}" placeholder="Day">
<input id="referenceEditorArchiveMonth" type="number" class="small" value="${data.month || ""}" placeholder="Month">
<input id="referenceEditorArchiveYear" type="number" class="small" value="${data.year || ""}" placeholder="Year">
</div>
</div>
<div>
<span class="title">URL status</span>
<div>
<select id="referenceEditorURLStatus">
<option name="live" ${data.status === "live" || !data.status ? "selected" : ""}>Live</option>
<option name="dead" ${data.status === "dead" ? "selected" : ""}>Dead</option>
</select>
</div>
</div>
`);
break;
case "publisher":
document.querySelector("#referenceArgs").insertAdjacentHTML("beforeend", `
<div>
<span class="title">Publisher</span>
<div>
<input id="referenceEditorPublisher" value="${escapeHTML(data.publisher || "")}">
</div>
</div>
`);
break;
default:
break;
}
}
if (document.readyState === "complete") {
refEditorLoadStylesheet();
}
window.addEventListener("load", refEditorLoadStylesheet);
function refEditorLoadStylesheet() {
const style = document.createElement("style");
style.innerHTML = `
#referenceArgs > div > span.title {
display: block;
margin: 2px 0;
font-weight: bold;
font-size: 0.9em;
width: 120px;
padding: 5px;
flex-shrink: 0;
}
#referenceArgs > div {
display: flex;
border-bottom: 1px solid #ddd;
}
#referenceArgs > div > div {
width: 100%;
display: flex;
align-items: center;
flex-wrap: wrap;
}
#referenceArgs textarea {
height: 100px;
}
#referenceArgs input.large {
width: 100%;
}
#referenceArgs input {
height: 100%;
box-sizing: border-box;
border: none;
outline: none !important;
}
#referenceArgs input.small {
width: 60px;
}
.referenceEditorAuthor {
margin: 5px 0;
display: flex;
}
#referenceEditorAuthors {
padding-bottom: 5px;
}
#referenceEditorButtons {
display: flex;
justify-content: flex-end;
}
#referenceEditorButtons button {
margin: 5px;
}
#additionalArgs span {
font-weight: bold;
cursor: pointer;
user-select: none;
display: inline-block;
margin-left: 10px;
font-size: 0.9em;
}
.referenceEditorArgTools {
position: absolute;
width: 100%;
display: flex;
justify-content: flex-end;
}
#referenceEditorCount {
position: fixed;
top: calc(100% - 50px);
left: 15px;
font-size: 0.9em;
user-select: none;
cursor: pointer;
}
`;
document.head.appendChild(style);
}
function escapeHTML(unsafe) {
if (!unsafe) {
return "";
}
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function addAdditionalAuthor() {
const elem = document.querySelector("#referenceEditorAuthors");
const author = document.createElement("div");
author.className = "referenceEditorAuthor";
author.innerHTML = `
<input placeholder="Last" class="referenceEditorLast">
<input placeholder="First" class="referenceEditorFirst">
<button onclick="this.parentElement.remove()">Remove</button>
`;
elem.insertBefore(author, elem.children[elem.children.length - 1]);
}
function getInputValue(id) {
id = "referenceEditor" + id;
return document.getElementById(id) ? document.getElementById(id).value : false;
}
function getAuthors() {
return [...document.querySelectorAll(".referenceEditorAuthor")]
.map(elem => {
const first = elem.querySelector(".referenceEditorFirst").value;
const last = elem.querySelector(".referenceEditorLast").value;
return !first || !last ? false : [ first, last ];
})
.filter(elem => elem);
}
function padNum(num, length) {
num = num.toString();
while (num.length < length) {
num = "0" + num;
}
return num;
}
function saveReference() {
const title = getInputValue("Title");
const website = getInputValue("Website");
const [ day, month, year ] = [ getInputValue("Day"), getInputValue("Month"), getInputValue("Year") ];
const work = getInputValue("Work");
const authors = getAuthors();
const publisher = getInputValue("Publisher");
const url = getInputValue("URL");
const archiveurl = getInputValue("ArchiveURL");
const [ aday, amonth, ayear ] = [ getInputValue("ArchiveDay"), getInputValue("ArchiveMonth"), getInputValue("ArchiveYear") ];
const urlstatus = (getInputValue("URLStatus") || "").toLowerCase();
const refType = document.querySelector("select[name=referenceType]").value;
const args = [];
const argumentsAvailable = referenceTemplateData[refType];
if (argumentsAvailable.includes("title") && title) {
args.push([ "title", title ]);
}
if (argumentsAvailable.includes("url") && url) {
args.push([ "url", url ]);
}
if (argumentsAvailable.includes("website") && website) {
args.push([ "website", website ]);
}
if (argumentsAvailable.includes("date") && day && month && year &&
day > 0 && day < 32 && month > 0 && month < 13) {
args.push([ "date", year + "-" + padNum(month, 2) + "-" + padNum(day, 2) ]);
}
if (argumentsAvailable.includes("archiveurl") && archiveurl && urlstatus && aday && amonth && ayear &&
aday > 0 && aday < 32 && amonth > 0 && amonth < 13) {
args.push([ "archive-url", archiveurl ]);
args.push([ "archive-date", ayear + "-" + padNum(amonth, 2) + "-" + padNum(aday, 2) ]);
args.push([ "url-status", urlstatus ]);
}
if (argumentsAvailable.includes("work") && work) {
args.push([ "work", work ]);
}
for (let i = 0; i < authors.length; i++) {
args.push([ "last" + (i + 1), authors[i][1] ]);
args.push([ "first" + (i + 1), authors[i][0] ]);
}
if (argumentsAvailable.includes("publisher") && publisher) {
args.push([ "publisher", publisher ]);
}
const additionalArgs = [...document.querySelectorAll("input[data-arg]")];
additionalArgs.forEach(arg => {
if (!arg.value) {
return;
}
args.push([ arg.attributes["data-arg"].value, arg.value ]);
});
const argText = args
.map(arg => `|${arg[0]}=${arg[1]}`)
.join(" ");
if (refType === "none") {
return document.querySelector("#referenceEditorWikitext").value;
}
return `{{cite ${refType} ${argText}}}`;
}
async function referenceFixerLoadArchive(button) {
button.innerText = "Loading...";
button.disabled = true;
const url = getInputValue("URL");
const archive = await getArchiveURL(url);
button.remove();
if (!url) {
return;
}
document.querySelector("#referenceEditorArchiveURL").value = archive.url;
document.querySelector("#referenceEditorArchiveDay").value = archive.day;
document.querySelector("#referenceEditorArchiveMonth").value = archive.month;
document.querySelector("#referenceEditorArchiveYear").value = archive.year;
}
async function getArchiveURL(url) {
try {
const response = await fetch("https://archive.org/wayback/available?url=" + url);
const json = await response.json();
if (!json["archived_snapshots"] || !json["archived_snapshots"]["closest"]) {
return { url: "", day: "", month: "", year: "" };
}
const { timestamp, url: archiveURL } = json["archived_snapshots"]["closest"];
const [_, year, month, day] = timestamp.match(/(\d{4})(\d{2})(\d{2})/);
return { url: archiveURL, day, month, year };
} catch (e) {
console.log("Could not fetch archive url: " + e);
return { url: "", day: "", month: "", year: "" };
}
}
async function saveButtonClicked(button) {
if (!currentlySelectedRef.item.replace && saveReference() !== currentlySelectedRef.item.wikitext) {
refsSaved++;
}
if (refsSaved === 1) {
document.body.insertAdjacentHTML("beforeend", `
<div id="referenceEditorCount" onclick="referenceEditorSave()"></div>
`);
}
if (refsSaved) {
document.querySelector("#referenceEditorCount").innerHTML = `
${refsSaved} reference${refsSaved === 1 ? "" : "s"} edited<br>
Click here to save
`;
}
currentlySelectedRef.item.replace = saveReference();
const refText = currentlySelectedRef.refElem.querySelector(".reference-text");
button.parentElement.parentElement.remove();
refText.innerHTML = "Loading...";
refText.innerHTML = await wikitextToHTML(currentlySelectedRef.item.replace);
refText.children[0].style.display = "inline";
}
async function referenceEditorSave() {
const page = await rfApi.get({
action: 'query',
prop: 'revisions',
rvprop: 'content',
titles: mw.config.get('wgPageName'),
formatversion: 2,
rvslots: '*'
});
let wikitext = page.query.pages[0].revisions[0].slots.main.content;
for (let item of editableReferences) {
if (!item.item.replace) {
continue;
}
wikitext = wikitext.replaceAll(item.item.wikitext, item.item.replace);
}
await rfApi.postWithEditToken({
"action": "edit",
"title": mw.config.get('wgPageName'),
"text": wikitext,
"summary": `Edited ${refsSaved} reference${refsSaved === 1 ? "" : "s"}`,
"format": "json"
});
location.reload();
}
async function wikitextToHTML(wikitext) {
let deferred = $.Deferred();
$.post("https://en.wikipedia.org/api/rest_v1/transform/wikitext/to/html",
"wikitext=" + encodeURIComponent(wikitext) + "&body_only=true",
function (data) {
deferred.resolve(data);
}
);
return deferred;
}
runReferenceEditor();
// </nowiki>
You must be logged in to post a comment.