MediaWiki:Gadget-AutoGlossary.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
// <nowiki>
mw.util.addCSS(`
	.mw-parser-output .auto-glossary a[rel~="mw:ExtLink"]:empty::after {
		content: '[' counter(mw-numbered-ext-link) ']';
		counter-increment: mw-numbered-ext-link;
	}
	.mw-parser-output .auto-glossary td:first-child {
		counter-reset: mw-numbered-ext-link; 
		text-align: center;
	}
	.mw-parser-output .auto-glossary ol {
		margin: 0 0 0 2em;
		padding: 0;
	}
`);

(async () => {
	for (let autoGlossary of document.querySelectorAll(".auto-glossary")) {
		let category = autoGlossary.getAttribute("data-category");
		let label = autoGlossary.getAttribute("data-label"); // The label parameter is currently unused.
		let lang = autoGlossary.getAttribute("data-language");
		let inlineOnly = autoGlossary.getAttribute("data-inline-only");

		let actionAPI = new mw.Api({ajax: {headers: {"Api-User-Agent": "Gadget developed by [[User:Ioaxxere]]"}}});
		let restAPI = new mw.Rest({ajax: {headers: {"Api-User-Agent": "Gadget developed by [[User:Ioaxxere]]"}}});
		let wordlist = [];
		let continueParam;

		// Get a list of words in the given category.
		while (true) {
			let response = await actionAPI.get({
				action: "query",
				list: "categorymembers",
				cmprop: "title",
				cmlimit: "max",
				cmtitle: "Category:" + category,
				cmcontinue: continueParam
			});

			// Only include entries from mainspace, appendix space, and reconstruction space.
			for (let entry of response.query.categorymembers) {
				if (entry.ns === 0 || entry.ns === 100 || entry.ns === 118)
					wordlist.push(entry.title);
			}

			if (!response.continue)
				break;

			continueParam = response.continue.cmcontinue;
		}

		wordlist.sort((a, b) => (/^[A-Za-z]/.test(a) ? /^[A-Za-z]/.test(b) ? a.localeCompare(b, undefined, {sensitivity: "base"}) : -1 : /^[A-Za-z]/.test(b) ? 1 : 0)); // ChatGPTsort

		let entriesLoaded = 0;
		let loadingIndicator = document.createElement("div");
		autoGlossary.append(loadingIndicator);

		let glossaryTable = document.createElement("table");
		glossaryTable.className = "wikitable";
		glossaryTable.style = "width: 100%; table-layout: fixed; display: table";
		glossaryTable.innerHTML = `<thead><tr><th style="width:8em">Terms</th><th>Definitions</th></tr></thead><tbody></tbody>`;

		let batchSize = 500;

		for (let batchStart = 0; batchStart < wordlist.length; batchStart += batchSize) {
			if (batchStart === batchSize)
				batchSize = 10e99; // Finish the rest of the list.

			let ongoingRequests = 0;
			let glossaryRows = await Promise.all(wordlist.slice(batchStart, batchStart + batchSize).map(async word => {
				// Limit concurrent requests, otherwise the browser might run of out memory.
				while (ongoingRequests >= 200)
					await new Promise(resolve => setTimeout(resolve, 10));

				ongoingRequests++;
				let response = await restAPI.get("/v1/page/" + encodeURIComponent(word) + "/html");
				ongoingRequests--;

				let responseDocument = new DOMParser().parseFromString(response, "text/html");
				let row;

				try {
					let langID = lang.replaceAll(" ", "_");
					let L2_section = responseDocument.getElementById(langID).parentNode;
					let glossaryDefs = [...L2_section.getElementsByTagName("li")].filter(li => {
						// Check if the li element contains an inline category.
						if ([...li.querySelectorAll(":scope > link[rel=\"mw:PageProp/Category\"], :scope > span > span > link[rel=\"mw:PageProp/Category\"]")]
							.some(linkElement => linkElement.getAttribute("href").startsWith("./Category:" + category.replaceAll(" ", "_")))) {
							return true;
						}

						// Check if the headword element contains the category.
						let headwordLine = li.parentElement.previousElementSibling;
						if (!inlineOnly && headwordLine && headwordLine.tagName === "P" &&
							[...headwordLine.querySelectorAll("link[rel=\"mw:PageProp/Category\"]")]
								.some(linkElement => linkElement.getAttribute("href").startsWith("./Category:" + category.replaceAll(" ", "_")))) {
							return true;
						}

						return false;
					});

					// If no definitions found: grab all top-level definitions.
					if (!inlineOnly && !glossaryDefs.length)
						glossaryDefs = L2_section.querySelectorAll(".mw-parser-output section > ol > li");

					// Insert a row if definitions were found.
					if (glossaryDefs.length) {
						let entryLink = document.createElement("a");
						entryLink.href = "https://en.wiktionary.org" + mw.util.getUrl(word) + "#" + langID;
						entryLink.textContent = word;

						let defList = document.createElement("ol");
						defList.append(...glossaryDefs);

						// Convert URLs into absolute URLs.
						for (let link of defList.querySelectorAll("a[href]"))
							link.href = new URL(link.getAttribute("href"), "https://en.wiktionary.org" + mw.util.getUrl(word)).href;

						// Clean HTML.
						defList.querySelectorAll("link, .previewonly, .maintenance-line").forEach(elem => elem.remove());
						for (let elem of defList.querySelectorAll("*")) {
							for (let attr of [...elem.attributes]) {
								if (attr.name !== "class" && attr.name !== "href" && attr.name !== "style" && attr.name !== "title" && attr.name !== "rel")
									elem.removeAttribute(attr.name);
							}
						}

						row = document.createElement("tr");
						row.insertCell(0).append(entryLink);
						row.insertCell(1).append(defList);
					}
				} catch (e) {
					console.log("auto-glossary.js could not parse: " + word);
				}
				entriesLoaded++;
				loadingIndicator.textContent = `Loaded entry: ${entriesLoaded}/${wordlist.length}`;
				return row;
			}));

			if (batchStart === 0)
				autoGlossary.append(glossaryTable);

			for (let row of glossaryRows.filter(Boolean)) {
				for (let quote of document.querySelectorAll("ol > li"))
					window.setupHiddenQuotes(quote);
				glossaryTable.querySelector("tbody").append(row);
			}
		}

		// Clear out loading display.
		for (let elem of [...autoGlossary.childNodes]) {
			if (elem !== glossaryTable)
				elem.remove();
		}
	}
})();
// </nowiki>