User:Ingenuity/ReferenceEditorTest.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Ingenuity/ReferenceEditorTest. |
// <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="loadReferenceData()">Load</button>
<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)) {
addAdditionalArg("archiveurl");
} 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>
`;
}
addAdditionalAuthor();
}
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;
}
async function loadReferenceData() {
const response = await fetch("http://127.0.0.1:8000/url?sitename=" + encodeURIComponent(currentlySelectedRef.item.url));
const data = await response.json();
if ("website" in data.metadata && data.metadata["website"]) {
document.querySelector(currentlySelectedRef.item.type === "news" ? "#referenceEditorWork" : "#referenceEditorWebsite").value = data.metadata["website"];
}
if ("title" in data.metadata && data.metadata["title"]) {
document.querySelector("#referenceEditorTitle").value = data.metadata["title"];
}
if ("date" in data.metadata && data.metadata["date"]) {
const date = new Date(data.metadata["date"]);
document.querySelector("#referenceEditorDay").value = date.getUTCDate();
document.querySelector("#referenceEditorMonth").value = date.getUTCMonth() + 1;
document.querySelector("#referenceEditorYear").value = date.getUTCFullYear();
}
if ("author" in data.metadata && data.metadata["author"] && data.metadata["author"].split(" ").length > 1) {
const author = data.metadata["author"].split(" ");
document.querySelector(".referenceEditorFirst").value = author[0];
document.querySelector(".referenceEditorLast").value = author[1];
}
if ("archive-url" in data.metadata && data.metadata["archive-url"]) {
const [ year, month, day ] = data.metadata["archive-date"].split("-");
document.querySelector("#referenceEditorArchiveURL").value = data.metadata["archive-url"];
document.querySelector("#referenceEditorArchiveDay").value = day;
document.querySelector("#referenceEditorArchiveMonth").value = month;
document.querySelector("#referenceEditorArchiveYear").value = year;
}
}
runReferenceEditor();
// </nowiki>