Jump to content

User:Ingenuity/ReferenceEditor.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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, "&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.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>