Royal Road: Add Word Count to Statistics Section

Adds the word count of a fiction (taken from the information tooltip in the Pages statistic) as it's own statistic

// ==UserScript==
// @name           Royal Road: Add Word Count to Statistics Section
// @namespace      https://github.com/w4tchdoge
// @version        1.1.0-20250611_121836
// @description    Adds the word count of a fiction (taken from the information tooltip in the Pages statistic) as it's own statistic
// @author         w4tchdoge
// @homepage       https://github.com/w4tchdoge/MISC-UserScripts
// @match          *://*.royalroad.com/fiction/*
// @exclude        *://*.royalroad.com/fiction/*/*/chapter/*
// @license        AGPL-3.0-or-later
// @run-at         document-start
// @history        1.1.0 — Add word count next to chapter count in the ToC header bar thing
// @history        1.0.0 — Initial release
// ==/UserScript==

(async function () {
	`use strict`;

	// modified from https://stackoverflow.com/a/61511955/11750206
	function waitForElm(selector, search_root = document) {
		return new Promise(resolve => {
			if (search_root.querySelector(selector)) {
				return resolve(search_root.querySelector(selector));
			}

			const observer = new MutationObserver(mutations => {
				if (search_root.querySelector(selector)) {
					observer.disconnect();
					resolve(search_root.querySelector(selector));
				}
			});

			// If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
			observer.observe(document.documentElement, {
				childList: true,
				subtree: true
			});
		});
	}

	// wait for fic info section to load in
	const ficinfo = await waitForElm(`div.fiction-info`);

	// Get the element which contains the word "Pages" and which also contains the word count in an informational tooltip
	const statsPage_pages_elem = ficinfo.querySelector(`.fiction-stats .stats-content .list-unstyled li:has(> i)`);

	// Get Word Count
	const word_count_str = statsPage_pages_elem.querySelector(`i`).getAttribute(`data-content`).replace(/.*calculated from (.*) words./gmi, `$1`);
	console.log(`
RR WORD COUNT SCRIPT
WORD COUNT: ${word_count_str}`);

	// Make the "Heading" element that describes the data below it. i.e. it says "Words"
	const elm_wordcount_title = (() => {
		let outelm = Object.assign(document.createElement(`li`), {
			id: `userscript-words-title`,
			innerHTML: `Words :`
		});

		// Add the bold and uppercase classes to make it look like the other stats page entries
		outelm.classList.add(`bold`, `uppercase`);

		return outelm;
	})();

	// Make the "Data" element that has the actual data. i.e. it says the word count
	const elm_wordcount_data = (() => {
		let outelm = Object.assign(document.createElement(`li`), {
			id: `userscript-words-data`,
			innerHTML: `${word_count_str}`
		});

		// Add the bold and uppercase classes to make it look like the other stats page entries
		outelm.classList.add(`bold`, `uppercase`, `font-red-sunglo`);

		return outelm;
	})();

	// Add the word count elements before the page count elements
	statsPage_pages_elem.before(elm_wordcount_title, elm_wordcount_data);

	const tocbar_ch_cnt = (() => {
		const tocbar = ficinfo.querySelector(`div.portlet > .portlet-title:has(> .caption > a#toc)`);
		const xpath = `.//*[contains(concat(" ",normalize-space(@class)," ")," actions ")]/span[contains(normalize-space(),"Chapter")]`;
		const chcntpar = document.evaluate(xpath, tocbar, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
		return chcntpar;
	})();

	const tocbar_wc = (() => {
		let base_elm = tocbar_ch_cnt.cloneNode(true);
		base_elm.textContent = `${word_count_str} Words`;
		base_elm.style.marginRight = `0.25em`;
		return base_elm;
	})();

	tocbar_ch_cnt.after(tocbar_wc);
})();