From Wikipedia, the free encyclopedia
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.pages0].revisions0].slots.main.content;

	

	const references = [...wikitext.matchAll(/<ref(?: name="?([^\/]+?)"?)?>(.+?)<\/ref>/gmsi)];

	

	const referenceArgs = references

		.map(ref => [...ref2].matchAll(/\|(?:\s+)?([^=]+?)(?:\s+)?=(?:\s+)?([^\|]+?)(\s+?)?(?=[\|]|(?:}}$))/gmsi)])

		.map(ref => ref.map(a => a1].toLowerCase(), a2]]));

	

	const cleanedRefs = [];

	

	for (let i = 0; i < references.length; i++) {

		const refUrl = referencesi][2].match(/https?:\/\/.+?(?=[\| }])/);

		const citeType = referencesi][2].match(/{{cite (.+?)(\s+)?(\||})/i);

		

		if (!refUrl) {

			continue;

		}

		

		cleanedRefs.push({

			type: citeType ? citeType1 : null,

			url: refUrl0],

			args: referenceArgsi],

			wikitext: referencesi][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 = editableReferencesnumber];

	

	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(editableReferencesnumber].item.type || "none");

	selectElem.value = editableReferencesnumber].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) {

		argDictitem0]] = item1];

	}

		

	for (let item of referenceTemplateDatatype]) {

		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(argDictitem])}">

				</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, "&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</button>

	`;

	elem.insertBefore(author, elem.childrenelem.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 = referenceTemplateDatarefType];

	

	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), authorsi][1 ]);

		args.push([ "first" + (i + 1), authorsi][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 => `|${arg0}=${arg1}`)

		.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.children0].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.pages0].revisions0].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>