User:Ingenuity/ReferenceEditor.js

//

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>/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 += ` `; }			}		}	} }

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 = `  No reference template Cancel Save `;	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 += ` Wikitext ${escapeHTML(currentlySelectedRef.item.wikitext)} `;				return; case "title": argsContainer.innerHTML += ` Title  `;				break; case "website": argsContainer.innerHTML += ` Website  `;				break; case "work": argsContainer.innerHTML += ` Work  `;				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 += ` Date    `;				break; case "url": argsContainer.innerHTML += ` URL  `;				break; case "authors": let authorCount = "first" in argDict || "first1" in argDict ? 1 : 0;				while ("first" + (authorCount + 1) in argDict) { authorCount++; }				let authorsHTML = ` Authors `;				for (let i = 0; i < authorCount; i++) { authorsHTML += `   <button onclick="this.parentElement.remove">Remove `;				}				authorsHTML += ` <span style="cursor: pointer; user-select: none; font-size: 0.9em;" onclick="addAdditionalAuthor">+ Add additional author `;				argsContainer.innerHTML += ` ${authorsHTML} `; break; case "publisher": if (!("publisher" in argDict)) { additionalContainer.innerHTML += ` <span onclick="addAdditionalArg('publisher'); this.remove;">+ Publisher `;				} 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 `;				} 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 += ` ${item} <input class="large" data-arg="${item}" value="${escapeHTML(argDict[item])}"> `;	} }

function addAdditionalArg(type, data) { data = data || {}; switch (type) { case "archiveurl": document.querySelector("#referenceArgs").insertAdjacentHTML("beforeend", `					 Archive URL 						<input id="referenceEditorArchiveURL" value="${escapeHTML(data.url || "")}" class="large">						<button onclick="referenceFixerLoadArchive(this)">Load 					 Archive date 						<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">					 URL status 						<select id="referenceEditorURLStatus">							<option name="live" ${data.status === "live" || !data.status ? "selected" : ""}>Live 							<option name="dead" ${data.status === "dead" ? "selected" : ""}>Dead 			`); break; case "publisher": document.querySelector("#referenceArgs").insertAdjacentHTML("beforeend", `					 Publisher 						<input id="referenceEditorPublisher" value="${escapeHTML(data.publisher || "")}">			`); 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, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;")		.replace(/'/g, "&#039;"); }

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 `;	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 ``; }

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"> 		`); }	if (refsSaved) { document.querySelector("#referenceEditorCount").innerHTML = ` ${refsSaved} reference${refsSaved === 1 ? "" : "s"} edited 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;

//