User:Kiril kovachev/common.js

From Wiktionary, the free dictionary
Jump to navigation Jump to search

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

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

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


// ------------------- 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("# {{lb|bg|relational}} {{rfdef|bg}}", "# {{lb|bg|relational}} " + bulgarianDefs[0].slice(2));
		} else {
			editor.replace(/({{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) {
		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
// {{l}} (link templates) to them, e.g.

// * {{l|bg|едно}}
// * {{l|bg|две}}
// * {{l|bg|три}}
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 = /<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);
	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{{es-pr}}\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 {{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===\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 `{{${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 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("{{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) {
				editor.replace("* {{R:bg:RBE}}\n", "");
			}
		});
	}
	
	getChitankaAsync(title).then((resp) => {
		if (!resp.ok) {
			editor.replace("* {{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 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 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);
});

// </nowiki>