User:Kiril kovachev/common.js

//

/* TODO: - Rework auto-insertion templates to work only on an existing Bulgarian section - Make reference template insert any missing references, even if a reference section already exists - Auto-add references / format page?

// Constants pertaining to references const CURRENT_BER_PAGE = 7; const CURRENT_BER_VOLUME = 1; const CURRENT_BTR_PAGE = 22; const GRAVE = String.fromCodePoint(0x300); const ACUTE = String.fromCodePoint(0x301);

// --PURE FUNCTIONS TO GENERATE PAGE DATA BEGIN HERE

//, , , , , and templates const der = (s, w) => derivationTemplateGeneric("der", s, w); const inh = (s, w) => derivationTemplateGeneric("inh", s, w); const bor = (s, w) => derivationTemplateGeneric("bor", s, w); const derP = (s, w) => derivationTemplateGeneric("der+", s, w); const inhP = (s, w) => derivationTemplateGeneric("inh+", s, w); const borP = (s, w) => derivationTemplateGeneric("bor+", s, w);

function referencesTemplate(berPageName) { return "\n===References===\n* \n* \n* \n* \n"; }

function bulgarianNounTemplate(stressed, gender) { return ( `==Bulgarian==

Etymology
From

${pronunciationTemplate(stressed)}

Noun

 * 1) definitions

Declension
`); }

function bulgarianVerbTemplate(stressed, aspect) { return ( `==Bulgarian==

Etymology
From

${pronunciationTemplate(stressed)}

Verb

 * 1) definitions

Conjugation
`); }

function bulgarianAdjectiveTemplate(stressed) { return ( `==Bulgarian==

Etymology
From

${pronunciationTemplate(stressed)}

Adjective

 * 1) definitions

Declension
`); }

function pronunciationTemplate(stressed) { return (`===Pronunciation=== }

// Depending on what source languages we've seen so far, // predict what language should be suggested next. /* Based on this order: Russian French Latin Ancient Greek

function predictNextEtymologyLanguage(metSoFar) { if (!metSoFar.includes("ru")) return "ru"; if (!metSoFar.includes("fr")) return "fr"; if (!metSoFar.includes("la")) return "la"; if (!metSoFar.includes("grc")) return "grc"; }

function getExistingEtymologyLanguages(page) { return [...page.matchAll(//g)].map((m)=>m[1]); }

function predictRelationalDeclension(relationalAdj) { if (relationalAdj.endsWith("ен")) { return "!*"; } else if (relationalAdj.endsWith("ов")) { return "!"; }	return ""; }

// Return format: // array of objects, each of which has a property // "lemma", which is a Bulgarian word/morpheme, // "t" (gloss), i.e. its meaning, // and "pos", part-of-speech or morphological information function predictRelationalForm(relationalAdj) { if (relationalAdj.endsWith("и́чен")) { const ichenPos = relationalAdj.indexOf("и́чен"); return [ {				lemma: relationalAdj.slice(0, ichenPos), t: undefined, // May perform scraping of Wiktionary for this in future pos: undefined },			{				lemma: "-и́чен", t: undefined, pos: "adjectival suffix" }		];	} else if (relationalAdj.endsWith("и́чески")) { const icheskiPos = relationalAdj.indexOf("и́чески"); return [ {				lemma: relationalAdj.slice(0, icheskiPos), t: undefined, // May perform scraping of Wiktionary for this in future pos: undefined },			{				lemma: "-и́чески", t: undefined, pos: "adjectival suffix" }		];	} else if (relationalAdj.endsWith("ен")) { const enPos = relationalAdj.indexOf("ен"); return [ {				lemma: relationalAdj.slice(0, enPos), t: undefined, // May perform scraping of Wiktionary for this in future pos: undefined },			{				lemma: "-ен", t: undefined, pos: "adjectival suffix" }		];	} else if (relationalAdj.endsWith("ски")) { const skiPos = relationalAdj.indexOf("ски"); return [ {				lemma: relationalAdj.slice(0, skiPos), t: undefined, // May perform scraping of Wiktionary for this in future pos: undefined },			{				lemma: "-ски", t: undefined, pos: "adjectival suffix" }		];	}	return null; }

const masculineSuffixes = [ "ар", "ач", "тел", "ин", "ик", "ец", "еж", ];

const feminineSuffixes = [ "ица", "ка", "ня", "ба", "ина", "ост", "ота" ];

const neuterSuffixes = [ "ище", "ство", "ние", "не", "ие", "че", "ле", "це" ]; function detectGender(title) { // Use some very basic logic to generally assume masculine, barring known exceptions if (endsInAnyOf(title, masculineSuffixes)) { return "m"; } else if (endsInAnyOf(title, feminineSuffixes)) { return "f"; } else if (endsInAnyOf(title, neuterSuffixes)) { return "n"; } else { return detectGenderByEndingPhonemes(title); } }

function detectGenderByEndingPhonemes(title) { if (endsInAnyOf(title, ["а", "я", "ст", "щ"])) { return "f"; } else if (endsInAnyOf(title, ["о", "е", "и", "у", "ю"])) { return "n"; } else { return "m"; } }

function endsInAnyOf(string, suffixesToTest) { for (let suffix of suffixesToTest) { if (string.endsWith(suffix)) { return true; }	}	return false; }

function startsWithAnyOf(string, prefixesToTest) { for (let prefix of prefixesToTest) { if (string.startsWith(prefix)) { return true; }	}	return false; }

function linkTemplate(term) { return ``; }

function generateAffixFromForm(form) { let out = []; out.push("affix"); out.push("bg"); for (let i = 0; i < form.length; i++) { out.push(form[i].lemma); }	for (let i = 0; i < form.length; i++) { if (form[i].t) { out.push(`t${i+1}=${form[i].t}`); }		if (form[i].pos) { out.push(`pos${i+1}=${form[i].pos}`); }	}	return ""; }

function getSection(source, level, sectionName) { const sectionPattern = new RegExp("^" + "=".repeat(level) + sectionName + "=".repeat(level) + "$", "gm"); const insubordinatePattern = new RegExp(`^={2,${level}}.+={2,${level}}$`, "gm"); const sectionStart = sectionPattern.exec(source).index; insubordinatePattern.lastIndex = sectionStart + 1; const m = insubordinatePattern.exec(source); const sectionEnd = m && m.index || -1; return source.slice(sectionStart, sectionEnd); }

// --- EDITOR-ENABLED FUNCTIONS BEGIN HERE function GetLemma(editor) { const IPATemplate = /\{\{bg-IPA\|(.+?)\}\}/; const pageNameMatch = IPATemplate.exec(editor.get); if (pageNameMatch) { // If e.g. IPA template has the stressed lemma inside, retrieve it		return pageNameMatch[1]; } else { // Just return the unstressed name for now return GetPageName(editor); }

}

function GetPageName(editor) { return document.getElementById("firstHeadingTitle").innerText; }

function GetTextArea { return document.getElementById("wpTextbox1"); }

// Given a string, if the string exists in the editor, it will be highlighted. function HighlightText(regex) { const textArea = GetTextArea; const match = regex.exec(textArea.value); if (!match) return; const selectionStart = match.index; const text = match[0]; textArea.focus; textArea.setSelectionRange(selectionStart, selectionStart + text.length); }

// Given a string, move the cursor after that string in the editor function MoveCursorAfter(regex) { const textArea = GetTextArea; const match = regex.exec(textArea.value); if (!match) return; const text = match[0]; const selectionStart = match.index + text.length; textArea.focus; textArea.setSelectionRange(selectionStart, selectionStart); }

function ReleaseSelection { const textArea = GetTextArea; const start = textArea.selectionStart; textArea.setSelectionRange(start, start); }

// Same as above, but move the cursor to the end of the current selection instead of the start function ReleaseSelectionAfter { const textArea = GetTextArea; const end = textArea.selectionEnd; textArea.setSelectionRange(end, end); }

function PortOverMeaning(editor, wiktPage) { getWiktionary(wiktPage).then((resp) => resp.json.then((json)=> { console.log(json); const src = json.parse.wikitext; const bulgarianDefsSrc = /==Bulgarian==[\s\S]+?#[\s\S]+?\n\n/.exec(src)[0]; const bulgarianDefs = [...bulgarianDefsSrc.matchAll(/# .+/g)].map((m) => m[0]); if (bulgarianDefs.length == 1) { editor.replace("# ", "#  " + bulgarianDefs[0].slice(2)); } else { editor.replace(/\n\n# /, "$1 \n\n" + bulgarianDefs.join("\n")); }	})); }

function InsertSortedL2(editor, content) { const page = editor.get; const allL2s = [...page.matchAll(/^==(.+)==$/gm)]; if (allL2s.length == 0) { editor.append(content); return; }	let i = 0; while (i < allL2s.length && allL2s[i][1] < "Bulgarian") { i++; }	if (i == allL2s.length) { editor.append("\n"); editor.append(content); } else { let index = page.indexOf(allL2s[i][0]); const inserted = page.slice(0, index) + content + "\n" + page.slice(index); editor.set(inserted); } }

// -INTERACTIVE FUNCTIONS BEGIN HERE--

// Returns an array of responses function PromptRepeatedly(promptStr) { let out = []; let answer; do { answer = prompt(promptStr); out.push(answer); } while (answer); return out.slice(0, -1); }

// Give in an array of prompts, an array of corresponding defaults for those prompts, // and get back an array of all the answers to each prompt in sequence function PromptMany(prompts, defaults) { let out = []; for (let i = 0; i < prompts.length; i++) { const response = prompt(prompts[i], defaults[i]); if (response) { out.push(response); } else { throw new Error("Prompt quit early"); }	}	return out; }

// Gets a sequence of user input words, and makes a list of // (link templates) to them, e.g.

// * // * // * function CreateLinkSection(title, promptText) { const terms = PromptRepeatedly(promptText); return `====${title}==== ${ terms.map(		(term) => "* " + linkTemplate(term)	).join("\n")}`; }

// --- SCRAPING STUFF BEGIN HERE -

// Synchronous web request might be better IMO as the data is needed // immediately for the prompt, e.g. when getting the stressed lemma from // Chitanka. function fetchSynchronous(url) { const request = new XMLHttpRequest; request.open("GET", "https://corsproxy.io/?" + url, false); // `false` makes the request synchronous request.send(null);

if (request.status === 200) { return request.responseText; } else { return null; } }

function fetchProxy(url) { return fetch(`https://corsproxy.io/?${url}`); }

// Return null if it isn't found function getRBE(word) { return fetchSynchronous(`https://rbe.chitanka.info/?q=${word}`); }

function getChitanka(word) { return fetchSynchronous(`https://rechnik.chitanka.info/w/${word}`); }

function getWiktionary(word) { return fetch(`https://en.wiktionary.org/w/api.php?action=parse&formatversion=2&page=${word}&prop=wikitext&format=json`); }

function getRBEAsync(word) { return fetchProxy(`https://rbe.chitanka.info/?q=${word}`); }

function getChitankaAsync(word) { return fetchProxy(`https://rechnik.chitanka.info/w/${word}`); }

// Given a word, get the version with stress mark according to Chitanka function getChitankaStress(word) { const chitankaText = getChitanka(word); if (!chitankaText) return; const m = /\s*(.+)\s*<\/span>/.exec(chitankaText.replace("&#768;", ACUTE)); return (m && m[1]) || undefined; }

// --TEMPLATESCRIPT SCRIPT FUNCTIONS BEGIN HERE ---

function References(editor) { if (editor.get.includes("===References===")) { return; }	const pageName = GetLemma(editor); const berPageName = pageName.replace(ACUTE, GRAVE); editor.append(referencesTemplate(berPageName)); // editor.setEditSummary("Added references"); }

function BulgarianNoun(editor) { const title = GetPageName(editor); const [stressed, gender] = PromptMany(		["Please enter the lemma: ",		 "Please enter the gender: "		],		[getChitankaStress(title) || title,		 detectGender(title)		]	); InsertSortedL2(editor, bulgarianNounTemplate(stressed, gender)); References(editor); HighlightText(/definitions/); }

function BulgarianVerb(editor) { const title = GetPageName(editor); const [stressed, aspect] = PromptMany(		["Please enter the lemma: ",		 "Please enter the aspect: "		],		[getChitankaStress(title) || title,		 "impf"		]	); InsertSortedL2(editor, bulgarianVerbTemplate(stressed, aspect)); References(editor); HighlightText(/definitions/); }

function BulgarianAdjective(editor) { const title = GetPageName(editor); const stressed = PromptMany(["Please enter the lemma: "], [getChitankaStress(title) || title]); InsertSortedL2(editor, bulgarianAdjectiveTemplate(stressed)); References(editor); HighlightText(/definitions/); }

// Note: not used right now. // Use when specifically editing a Spanish entry, currently works only if it has an L3 Etymology header. I'll figure out the JavaScript for this later. function EsPr(editor) { if (editor.get.includes("Pronunciation")) { return; } const EtymologySection = RegExp("(===Etymology===\n.+)\n\n", 'g'); editor .replace(			EtymologySection,			"$1\n\n===Pronunciation===\n\n\n"			); // .setEditSummary("Add pronunciation"); }

function BgEtymology(editor) { if (!editor.get.includes("Etymology")) { const BulgarianSection = RegExp("(==Bulgarian==\n)", 'g'); editor .replace(				BulgarianSection,				"$1\n===Etymology===\nFrom \n"				); // .setEditSummary("Added etymology"); }

// Move to edit the default etymology stub MoveCursorAfter(/(?<====Etymology===\n)[^\n]*(?=\s*===)/); }

// Derivation function takes a source language code and a word and produces // an etymology template like function DerivationGeneric(editor, derivationFunction, plus=false) { const predictedLanguage = predictNextEtymologyLanguage(getExistingEtymologyLanguages(editor.get)); const [sourceLanguage, sourceWord] = PromptMany(		["Enter source language: ",		 "Enter etymon: "		],		[predictedLanguage,		 (predictedLanguage === "ru") ? GetLemma(editor) : undefined		]	); const derivationOutput = derivationFunction(sourceLanguage, sourceWord); const BareEtymology = "===Etymology===\nFrom \n"; if (plus && editor.contains(BareEtymology)) { console.log("Bare etymology found"); editor.replace(BareEtymology, "===Etymology===\n" + derivationOutput + "\n"); } else { editor.insertAtCursor(derivationOutput); } }

function derivationTemplateGeneric(templateName, sourceLanguage, sourceWord) { return ``; }

const Derived = (editor) => DerivationGeneric(editor, der); const Borrowed = (editor) => DerivationGeneric(editor, bor); const Inherited = (editor) => DerivationGeneric(editor, inh); const DerivedPlus = (editor) => DerivationGeneric(editor, derP, true); const BorrowedPlus = (editor) => DerivationGeneric(editor, borP, true); const InheritedPlus = (editor) => DerivationGeneric(editor, inhP, true);

function AddDerivedTerms(editor) { if (!editor.get.includes("Derived terms")) { const derivedTermsSection = CreateLinkSection("Derived terms", "Please enter derived term: "); const Declension = RegExp("(====Declension====\n.+}}\n)", 'g'); editor .replace(				Declension,				"$1\n" + derivedTermsSection + "\n"				); // .setEditSummary("Added derived terms"); } }

function AddRelatedTerms(editor) { if (!editor.get.includes("Related terms")) { const relatedTermsSection = CreateLinkSection("Related terms", "Please enter related term: "); const References = RegExp("(===References===)", 'g'); editor .replace(				References,				relatedTermsSection + "\n\n$1"				); // .setEditSummary("Added derived terms"); } }

// When creating an accelerated entry for a relational adjective, // this template will handle filling in as much as possible for the // usual forms. function AutocompleteRelationalAdj(editor) { editor.replace(/ */g, ""); // Remove comments

const lemma = GetLemma(editor); const declSpec = predictRelationalDeclension(lemma); editor.replace(/<.*>/, `<${declSpec}>`); // Predict declension spec const form = predictRelationalForm(lemma); if (form) { editor.replace("", `From ${generateAffixFromForm(form)}.`); PortOverMeaning(editor, form[0].lemma.replace(ACUTE, "")); // Copy the definitions over from the original Wiktionary page } }

function PurgeReferences(editor) { const title = GetPageName(editor); if (startsWithAnyOf(title, ["а", "б", "в", "г", "д", "е", "н", "о", "п"])) { getRBEAsync(title).then((resp) => {			if (!resp.ok) {				editor.replace("* \n", "");			}		}); }	getChitankaAsync(title).then((resp) => {		if (!resp.ok) {			editor.replace("* \n", "");		}	}); }

// -- TEMPLATESCRIPT TEMPLATE OBJECTS BEGIN HERE --

const EsPrTemplate = { name: "Add es-pr", isMinorEdit: false, enabled: false, category: "One-click edits", script: EsPr };

const BgReferencesTemplate = { name: "Add references", isMinorEdit: false, enabled: true, category: "Fill-in edits", script: References, };

const BgEtymologyTemplate = { name: "Add etymology", isMinorEdit: false, enabled: true, category: "Fill-in edits", script: BgEtymology };

const BulgarianNounTemplate = { name: "Bulgarian noun", isMinorEdit: false, enabled: true, category: "Fill-in edits", editSummary: "Create Bulgarian noun", script: BulgarianNoun };

const BulgarianVerbTemplate = { name: "Bulgarian verb", isMinorEdit: false, enabled: true, category: "Fill-in edits", editSummary: "Create Bulgarian verb", script: BulgarianVerb };

const BulgarianAdjectiveTemplate = { name: "Bulgarian adjective", isMinorEdit: false, enabled: true, category: "Fill-in edits", editSummary: "Create Bulgarian adjective", script: BulgarianAdjective };

const DerTemplate = { name: "Add template", isMinorEdit: false, enabled: true, category: "Fill-in edits", script: Derived, };

const BorTemplate = { name: "Add template", isMinorEdit: false, enabled: true, category: "Fill-in edits", script: Borrowed, };

const InhTemplate = { name: "Add template", isMinorEdit: false, enabled: true, category: "Fill-in edits", script: Inherited, }; const DerPlusTemplate = { name: "Add template", isMinorEdit: false, enabled: true, category: "Fill-in edits", script: DerivedPlus, };

const BorPlusTemplate = { name: "Add template", isMinorEdit: false, enabled: true, category: "Fill-in edits", script: BorrowedPlus, };

const InhPlusTemplate = { name: "Add template", isMinorEdit: false, enabled: true, category: "Fill-in edits", script: InheritedPlus, };

const DerivedTermsTemplate = { name: "Add derived terms", isMinorEdit: false, enabled: true, category: "Fill-in edits", script: AddDerivedTerms, };

const RelatedTermsTemplate = { name: "Add related terms", isMinorEdit: false, enabled: true, category: "Fill-in edits", script: AddRelatedTerms, };

const AutocompleteRelationalAdjTemplate = { name: "Autodetect relational adjective", isMinorEdit: false, enabled: true, category: "One-click edits", script: AutocompleteRelationalAdj, editSummary: "Create relational adjective" };

const PurgeReferencesTemplate = { name: "Purge dead references", isMinorEdit: false, enabled: true, category: "One-click edits", script: PurgeReferences };

// ---MAIN DRIVER CODE BEGINS HERE---

// List of template objects to add to the menu const TEMPLATES = [ EsPrTemplate, BulgarianNounTemplate, BulgarianVerbTemplate, BulgarianAdjectiveTemplate, BgReferencesTemplate, BgEtymologyTemplate, DerTemplate, DerPlusTemplate, BorTemplate, BorPlusTemplate, InhTemplate, InhPlusTemplate, DerivedTermsTemplate, RelatedTermsTemplate, AutocompleteRelationalAdjTemplate, PurgeReferencesTemplate ];

function applyTemplate(template) { template(pathoschild.TemplateScript.Context); }

mw.config.set('userjs-templatescript', { regexEditor: false }); $.ajax("//tools-static.wmflabs.org/meta/scripts/pathoschild.templatescript.js",	{		dataType: "script",		cache: true	}) .then(function { // TEMPLATESCRIPT HACKS AND POLYFILLS INTERCEDE HERE- // (These should go here to ensure that the rest of the templates and so on // definitely have access to the extra functions I'm definding)

pathoschild.TemplateScript.Context.insertAtCursor = function(text) { this.replaceSelection(text); // Advance the cursor to after the inserted text const textArea = GetTextArea; const start = textArea.selectionStart; const end = start + text.length; textArea.setSelectionRange(end, end); };

// - DRIVER CODE RESUMES --- pathoschild.TemplateScript.add(TEMPLATES); function customEditorKeybinds(event) { console.log(event); if (event.ctrlKey) { if (event.key == "F1") { applyTemplate(BulgarianNoun); }			if (event.key == "F2") { applyTemplate(BulgarianVerb); }			if (event.key == "F3") { applyTemplate(BulgarianAdjective); }			if (event.key == "e") { applyTemplate(BgEtymology); event.preventDefault; return false; }			if (event.key == ",") { // Edit the declension spec HighlightText(/(?<=<)[^<>\s]*(?=>)/); event.preventDefault; return false; }			if (event.key == "r") { applyTemplate(References); event.preventDefault; return false; }			if (event.key == "d") { applyTemplate(Derived); }			if (event.key == "i") { applyTemplate(Inherited); }			if (event.key == "b") { applyTemplate(Borrowed); }			if (event.key == "D") { applyTemplate(DerivedPlus); }			if (event.key == "I") { applyTemplate(InheritedPlus); }			if (event.key == "B") { applyTemplate(BorrowedPlus); }			if (event.key == "y") { applyTemplate(AddDerivedTerms); event.preventDefault; }			if (event.key == "Y") { applyTemplate(AddRelatedTerms); event.preventDefault; }			if (event.key == ".") { applyTemplate(AutocompleteRelationalAdj); }			if (event.key == "Escape") { ReleaseSelectionAfter; return; }		}		if (event.key == "Escape") { ReleaseSelection; }	}	document.addEventListener("keydown", customEditorKeybinds, false); });

//