User:Ingenuity/ReferenceEditor.js

// <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>

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.