Jump to content

User:Kiril kovachev/common.js

From Wiktionary, the free dictionary

Note: You may have to bypass your browser’s cache to see the changes. In addition, after saving a sitewide CSS file such as MediaWiki:Common.css, it will take 5-10 minutes before the changes take effect, even if you clear your cache.

  • Mozilla / Firefox / Safari: hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Command-R on a Macintosh);
  • Konqueror and Chrome: click Reload or press F5;
  • Opera: clear the cache in Tools → Preferences;
  • Internet Explorer: hold Ctrl while clicking Refresh, or press Ctrl-F5.

This JavaScript is executed for Kiril kovachev on every page load.


// <nowiki>

/* 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?
	- Add auto-detection of common {{affix}}-based etymologies
	- Fix shortcuts for derived/related terms
	- Override the bold shortcut from the editor
	- Fix the relational adjective auto-detection (broken)
	- Fix the BER fetching (broken?)
	- Make some kind of system for auto-creating adjective forms on click (via KovachevBot)
	- Make more motions / shortcuts that take me to defined places
	-- The nearest definition line
	- Copy over related terms from another article using a command
	- Allocate more shortcuts and unbind my browser ones
	- Tangential: finish and use bg-pr
	- Add EditorReplaceAll to permit multiple regex subs
*/

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

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

// {{der}}, {{inh}}, {{bor}}, {{der+}}, {{inh+}}, and {{bor+}} 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* {{R:bg:RBE}}\n* {{R:bg:RBE2}}\n* {{R:bg:BER|" + berPageName + "|" + CURRENT_BER_PAGE + "|" + CURRENT_BER_VOLUME + "}}\n* {{R:bg:BTR|page=" + CURRENT_BTR_PAGE + "}}\n";
}

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

===Etymology===
From 

${pronunciationTemplate(stressed)}

===Noun===
{{bg-noun|${stressed}|${gender}}}

# definitions

====Declension====
{{bg-ndecl|${stressed}<>}}
`);
}

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

===Etymology===
From 

${pronunciationTemplate(stressed)}

===Verb===
{{bg-verb|${stressed}|${aspect}}}

# definitions

====Conjugation====
{{bg-conj|${stressed}<>}}
`);
}

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

===Etymology===
From

${pronunciationTemplate(stressed)}

===Adjective===
{{bg-adj|${stressed}}}

# definitions

====Declension====
{{bg-adecl|${stressed}<>}}
`);
}

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

===Etymology===
From

${pronunciationTemplate(stressed)}

===Adverb===
{{bg-adv|${stressed}}}

# definitions
`);
}


function pronunciationTemplate(stressed) {
	return (`===Pronunciation===
* {{bg-IPA|${stressed}}}
* {{bg-hyph}}`);
}

// 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(/{{(?:der|inh|bor)\+?\|bg\|(\w{2,3}).*?}}/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 `{{l|bg|${term}}}`;
}

function alterTemplate(term) {
	return `{{alter|bg|${term}}}`;
}

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 "{{" + out.join("|") + "}}";
}

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

function calculateAdditions(stateBefore, stateAfter) {
	function diffHas(pattern) {
		return stateBefore.search(pattern) === -1 && stateAfter.search(pattern) !== -1;
	}
	
	const additions = [];
	for (const [pattern, name] of [
			["===Etymology===", "etymology"],
			["===Pronunciation===", "pronunciation"],
			["===References===", "references"],
			["===Alternative forms===", "alternative forms"],
			["====Related terms====", "related terms"],
			["====Derived terms====", "derived terms"],
			["{{syn|bg", "synonyms"],
			["{{ant|bg", "antonyms"],
			["{{cot|bg", "coordinate terms"],
			["{{audio|bg|", "audio"],
	]) {
			if (diffHas(pattern)) {
				additions.push(name);
			}
	}

	return additions;
}

function detectPartsOfSpeechAdded(state) {
	const added = [];
	for (const pos of ["Noun", "Adjective", "Verb", "Adverb"]) {
		if (state.includes(`===${pos}===`)) {
			added.push(post.toLowerCase());
		}
	}
	return added;
}

function calculateEditSummary(stateBefore, stateAfter) {
	if (IsNewEntry()) {
		const added = detectPartsOfSpeechAdded(stateAfter);
		if (added.length > 0) {
			return `Created Bulgarian ${added.join(" + ")}`;
		} else {
			return null;
		}
	} else {
		const added = calculateAdditions(stateBefore, stateAfter);
		if (added.length > 0) {
			return `Added ${added.join(", ")}`;
		} else {
			return null;
		}
	}
}

// ------------------- 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 GetBulgarian(editor) {
	const BulgarianSection = /==Bulgarian==\n[\s\S]+?(?=(==)[^=]+\1(?=\n))|==Bulgarian==\n[\s\S]+$/;
	return (BulgarianSection.exec(editor.get()) || [null])[0];
}

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

function IsNewEntry() {
	return window.location.href.includes("redlink=1");
}

function FindBgSectionNum(editor) {
	const sectionPattern = /(=)(.+)\1/;
	const sections = [...editor.get().matchAll(/(=+)([^=]+)\1(?=\n)/g).map(m => m[2])];
	return sections.indexOf("Bulgarian");
}

function EditBgOnly(editor) {
	if (!IsNewEntry() && !window.location.href.includes("section=")) {
		const bgSectionNum = FindBgSectionNum(editor);
		if (bgSectionNum === -1)
			return;
		window.location.href = `https://en.wiktionary.org/w/index.php?title=${GetPageName()}&action=edit&section=${bgSectionNum+1}`;
	}
}

function Find() {
	const pattern = new RegExp(prompt("Search: "));
	HighlightText(pattern);
}

// Given a string, if the string exists in the editor, it will be highlighted.
// Return the regex match or undefined.
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);
	
	return match;
}

// Assuming text is select, replace that selection with the input.
// If the cursor is just floating with no selection, then it will input the text wherever
// the cursor is.
function ReplaceText(text) {
	GetTextArea().focus();
    document.execCommand('insertText', false, text);  // FIXME: DEPRECATED!
    // But, there is no replacement for execCommand at this stage (Dec 2024).
}

// Given a regex pattern or string, select the text and replace it with the replacement.
function EditorReplace(search, replacement, removeLookaround=false) {
	let regex = search instanceof RegExp ? search : new RegExp(search);
	const match = HighlightText(regex);
	
	// Remove RegExp lookaround, which is necessary because we can't "select" the entire
	// text which triggered the match in HighlightText. The alternative is to just
	// perform the replacement on the entire editor, but that seems less wise to me.
	// Well, this is also rather hacky...
	if (removeLookaround) {
		const regexStr = regex.toString().slice(1,-1);
		const regexStrFixed = regexStr.replace(/\(\?<?[=!].+?\)/, "");
		regex = new RegExp(regexStrFixed);
	}
	
	const out = match[0].replace(regex, replacement);  // Expand the regex patterns
	ReplaceText(out);
}

function EditorSet(text) {
	HighlightText(/[\s\S]*/);
	ReplaceText(text);
}

function EditorAppend(text) {
	const textarea = GetTextArea();
	const current = textarea.value;
	textarea.setSelectionRange(current.length, current.length);
	ReplaceText(text);
}

// 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) {
			EditorReplace("# {{lb|bg|relational}} {{rfdef|bg}}", "# {{lb|bg|relational}} " + bulgarianDefs[0].slice(2));
		} else {
			EditorReplace(/({{bg-adj.+?}})\n\n# {{lb\|bg\|relational}} {{rfdef\|bg}}/, "$1 {{tlb|bg|relational}}\n\n" + bulgarianDefs.join("\n"));
		}
	}));
}

function InsertSortedL2(editor, content) {
	const page = editor.get();
	const allL2s = [...page.matchAll(/^==(.+)==$/gm)];
	if (allL2s.length == 0) {
		EditorAppend(content);
		return;
	}
	let i = 0;
	while (i < allL2s.length && allL2s[i][1] < "Bulgarian") {
		i++;
	}
	if (i == allL2s.length) {
		EditorAppend("\n");
		EditorAppend(content);
	} else {
		let index = page.indexOf(allL2s[i][0]);
		const inserted = page.slice(0, index) + content + "\n" + page.slice(index);
		EditorSet(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;
}

function CreateTitle(level, title) {
	return `${'='.repeat(level)}${title}${'='.repeat(level)}`;
}

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

// * {{l|bg|едно}}
// * {{l|bg|две}}
// * {{l|bg|три}}
function CreateLinkSection(title, promptText, template=linkTemplate, level=4) {
	const terms = PromptRepeatedly(promptText);
	const sectionTitle = CreateTitle(level, title);
	return `${sectionTitle}
${  terms.map(
		(term) => "* " + template(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 getRequestJSON(url) {
	return JSON.stringify({
		"kovachev_url": url,
		"kovachev_password": localStorage.getItem("password") || ""
	});
}

const CORS_URL = "https://cors.kovachev.xyz/";
const CONTENT_TYPE = "application/json;charset=UTF-8";

function fetchSynchronous(url) {
	const request = new XMLHttpRequest();
	request.open("POST", CORS_URL, false); // `false` makes the request synchronous
	request.setRequestHeader("Content-Type", CONTENT_TYPE);
	request.send(getRequestJSON(url));

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

function fetchProxy(url) {
	return fetch(CORS_URL, {
		method: "post",
	    headers: {
	    	"Content-Type": CONTENT_TYPE
	    },
	    body: getRequestJSON(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 = /<span id="\w+-stressed_.+">\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);
	const outputText = referencesTemplate(berPageName);

	// Source: https://github.com/doozan/wikibot/blob/46b8275a156cce8aef061018dde70d16c6b07e94/fix_es_forms.py#L1127
	// by JeffDoozan
	// const catTemplates = [ "c", "C", "cat", "top", "topic", "topics", "categorize", "catlangname", "catlangcode", "cln", "DEFAULTSORT" ];
	// const reTemplates = "\\{\\{\s*(" + catTemplates.join("|") + ")\\s*[|}][^{}]*\\}*";
	// const reCategories = "\\[\\[\\s*Category\\s*:[^\\]]*\\]\\]";
	// const categoryPattern = new RegExp(`(?s)(.*?\\n)((\\s*(${reTemplates}|${reCategories})\\s*)*)$`);
	const categoryPattern = /\n((?:\s*(?:\{\{\s*(?:c|C|cat|top|topic|topics|categorize|catlangname|catlangcode|cln|DEFAULTSORT)\s*[|}][^{}]*\}*|\[\[\s*Category\s*:[^\]]*\]\])\s*)*)$/;

	if (editor.get().includes("===Anagrams===")) {
		const AnagramsSection = RegExp("\n(===Anagrams===\n.+\n)", 'g');
		EditorReplace(AnagramsSection, `${outputText}\n$1`);
	} else if (categoryPattern.exec(editor.get()) /*includes category syntax*/) {
		EditorReplace(categoryPattern, `\n${outputText}$1`);		
	} 
	else {
		EditorAppend(outputText);
	}
	// 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/);
}

function BulgarianAdverb(editor) {
	const title = GetPageName(editor);
	const stressed = PromptMany(["Please enter the lemma: "], [getChitankaStress(title) || title]);
	InsertSortedL2(editor, bulgarianAdverbTemplate(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');
	EditorReplace(
			EtymologySection,
			"$1\n\n===Pronunciation===\n{{es-pr}}\n\n"
			);
		// .setEditSummary("Add pronunciation");
}

function BgEtymology(editor) {
	if (!editor.get().includes("Etymology")) {
		const BulgarianSection = /(==Bulgarian==\n(?:\{\{(?:wikipedia|wp)\|.+?\}\}\n)?)/;
		EditorReplace(
				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 {{der|bg|grc|...}}
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===\n)From /;
	if (plus && editor.contains(BareEtymology)) {
		console.log("Bare etymology found");
		EditorReplace(BareEtymology, derivationOutput, removeLookaround=true);
	} else {
		ReplaceText(derivationOutput);
	}
}

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

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 predictAffixElements(lemma) {
	return null;
}

function autoGlossCommonAffixes(affixes) {
	// No-op (WIP)
	return affixes;
}

function AddAffix(editor) {
	const predictedAffixElements = predictAffixElements(GetLemma(editor));
	let affixElements;
	if (predictedAffixElements === null) {
		// Get affix elements from the user directly
		const userAffixElements = PromptRepeatedly("Enter prompt elements: ");
		const autoGlossed = userAffixElements.map(autoGlossCommonAffixes);
		affixElements = autoGlossed;
	} else {
		affixElements = predictedAffixElements;
	}
	
	const affixTemplateStr = `{{affix|bg|${affixElements.join('|')}}}`;
	ReplaceText(affixTemplateStr);
}

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');
		EditorReplace(
				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');
		EditorReplace(
				References,
				relatedTermsSection + "\n\n$1"
				);
			// .setEditSummary("Added derived terms");
	}
}

function AddAlternativeForms(editor) {
	if (!editor.get().includes("Alternative forms")) {
		const alternativeFormsSection = CreateLinkSection("Alternative forms", "Please enter alternative form: ", template=alterTemplate, level=3);
		const Etymology = RegExp("(===Etymology===\n.+\n)", 'g');
		EditorReplace(
				Etymology,
				alternativeFormsSection + "\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) {
	EditorReplace(/ *<!--.*-->/g, "");  // Remove comments

	const lemma = GetLemma(editor);
	const declSpec = predictRelationalDeclension(lemma);
	EditorReplace(/<.*>/, `<${declSpec}>`);  // Predict declension spec
	const form = predictRelationalForm(lemma);
	if (form) {
		EditorReplace("{{rfe|bg}}", `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) {
				EditorReplace("* {{R:bg:RBE}}\n", "");
			}
		});
	}
	
	getChitankaAsync(title).then((resp) => {
		if (!resp.ok) {
			EditorReplace("* {{R:bg:RBE2}}\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 BulgarianAdverbTemplate = {
	name: "Bulgarian adverb",
	isMinorEdit: false,
	enabled: true,
	category: "Fill-in edits",
	editSummary: "Create Bulgarian adverb",
	script: BulgarianAdverb
};


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

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

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

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

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

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

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 AlternativeFormsTemplate = {
	name: "Add alternative forms",
	isMinorEdit: false,
	enabled: true,
	category: "Fill-in edits",
	script: AddAlternativeForms,
};

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,
	BulgarianAdverbTemplate,
	BgReferencesTemplate,
	BgEtymologyTemplate,
	AddAffixTemplate,
	DerTemplate,
	DerPlusTemplate,
	BorTemplate,
	BorPlusTemplate,
	InhTemplate,
	InhPlusTemplate,
	DerivedTermsTemplate,
	RelatedTermsTemplate,
	AlternativeFormsTemplate,
	AutocompleteRelationalAdjTemplate,
	PurgeReferencesTemplate
];

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

let editorStateBefore;
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 defining)
	const editor = pathoschild.TemplateScript.Context;
	editorStateBefore = editor.get();
// ------------------------- 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 == "A") {
				applyTemplate(AddAffix);
			}
			if (event.key == "d") {
				applyTemplate(Derived);
			}
			if (event.key == "i") {
				applyTemplate(Inherited);
			}
			if (event.key == "b") {
				applyTemplate(Borrowed);
				event.preventDefault();
				return false;
			}
			if (event.key == "D") {
				applyTemplate(DerivedPlus);
			}
			if (event.key == "I") {
				applyTemplate(InheritedPlus);
			}
			if (event.key == "B") {
				applyTemplate(BorrowedPlus);
			}
			if (event.key == "m") {
				applyTemplate(AddDerivedTerms);
				event.preventDefault();
			}
			if (event.key == "M") {
				applyTemplate(AddRelatedTerms);
				event.preventDefault();
			}
			if (event.key == ".") {
				applyTemplate(AutocompleteRelationalAdj);
			}
			if (event.key == "'") {
				applyTemplate(AddAlternativeForms);
			}
			if (event.key == "Escape") {
				ReleaseSelectionAfter();
				return;
			}
			if (event.key == ";") {
				applyTemplate(EditBgOnly);
			}
			if (event.key == "f") {
				Find();
				event.preventDefault();
				return false;
			}
		}
		if (event.key == "Escape") {
			ReleaseSelection();
		}
	}
	
	document.addEventListener("keydown", customEditorKeybinds, false);
	document.getElementById("wpSave").addEventListener("mouseover", function(){
		const editorStateAfter = editor.get();
		const editMessage = calculateEditSummary(editorStateBefore, editorStateAfter);
		if (editMessage !== null) {
			editor.setEditSummary(editMessage);
		}
	});
});

// </nowiki>