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 = `

`;

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

`;

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 += `

`;

}

authorsHTML += `

+ Add additional author

`;

argsContainer.innerHTML += `

${authorsHTML}
`;

break;

case "publisher":

if (!("publisher" in argDict)) {

additionalContainer.innerHTML += `

+ Publisher

`;

} else {

addAdditionalArg("publisher", { publisher: argDict["publisher"] });

}

break;

case "archiveurl":

if (!("archiveurl" in argDict) && !("archive-url" in argDict)) {

additionalContainer.innerHTML += `

+ 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}

`;

}

}

function addAdditionalArg(type, data) {

data = data || {};

switch (type) {

case "archiveurl":

document.querySelector("#referenceArgs").insertAdjacentHTML("beforeend", `

Archive URL

Archive date

URL status

`);

break;

case "publisher":

document.querySelector("#referenceArgs").insertAdjacentHTML("beforeend", `

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, "&")

.replace(/

.replace(/>/g, ">")

.replace(/"/g, """)

.replace(/'/g, "'");

}

function addAdditionalAuthor() {

const elem = document.querySelector("#referenceEditorAuthors");

const author = document.createElement("div");

author.className = "referenceEditorAuthor";

author.innerHTML = `

`;

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", `

`);

}

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();

//