AO3: Get Current Chapter Word Count

Counts and displays the number of words in the current chapter

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name           AO3: Get Current Chapter Word Count
// @namespace      https://github.com/w4tchdoge
// @version        1.2.1-20240705_232304
// @description    Counts and displays the number of words in the current chapter
// @author         w4tchdoge
// @homepage       https://github.com/w4tchdoge/MISC-UserScripts
// @match          *://archiveofourown.org/*chapters/*
// @match          *://archiveofourown.org/*works/*
// @exclude        *://archiveofourown.org/*works/*/bookmarks
// @exclude        *://archiveofourown.org/*works/*/navigate
// @icon           https://archiveofourown.org/favicon.ico
// @license        AGPL-3.0-or-later
// @history        1.2.1 — Prevent script from running on multi-chapter works which only have 1 chapter published.
// @history        1.2.0 — Replace \w with [\p{Letter}\p{Mark}\p{Number}\p{Connector_Punctuation}] in the regular expession as that is the proper JavaScript equivalent to Ruby's [[:word:]]. Add support for most Unicode scripts supported in regular expressions. Use Array.from() instead of the spread syntax to convert the RegExpStringIterator into a countable array. Add *://archiveofourown.org/*chapters/* as a @match rule so that the script can work on URLs such as https://archiveofourown.org/chapters/141182779. Add *://archiveofourown.org/*works/*/navigate as an @exclude rule so the script does not run on the index page.
// @history        1.1.3 — Get rid of the element containing the words "Chapter Text" using removeChild() so I don't have to use RegEx to get rid of it. Also some miscellaneous cleanup
// @history        1.1.2 — Switch to using Intl.NumberFormat for making the word count thousands separated
// @history        1.1.1 — Modify the match rule so that it matches collections/*/works URLs as well; Add an exlude role so it doesn't work on works/*/bookmarks pages as it isn't designed to
// @history        1.1.0 — Implement a counting method that uses an attempted conversion of the Ruby regex code used by AO3 to JavaScript
// ==/UserScript==

(function () {
	`use strict`;

	// Get the current chapter count as a integer number
	const curr_chp_cnt = parseInt(document.querySelector(`dd.stats dd.chapters`).textContent.split(`/`).at(0));

	// Execute script only on multi-chapter works which have more than one chapter published and only when a single chapter is being viewed
	if (window.location.pathname.toLowerCase().includes(`chapters`) && curr_chp_cnt > 1) {

		// Get the Chapter Text
		const chapter_text = (function () {
			// Get the HTML element containing the chapter's text content
			let elm_parent = document.querySelector(`[role="article"]:has(> #work)`).cloneNode(true);
			// Remove the child element with the text "Chapter Text"
			elm_parent.removeChild(elm_parent.querySelector(`#work`));

			// Return only the textContent of the HTML element
			return elm_parent.textContent.trim();
		})();

		// Couting and formatting the number of words
		const word_count = (function () {

			// Attempted conversion of the Ruby regex code AO3 uses to JavaScript by looking at:
			// https://github.com/otwcode/otwarchive/blob/943f585818005be8df269d84ca454af478150e75/config/config.yml#L453
			// https://github.com/otwcode/otwarchive/blob/943f585818005be8df269d84ca454af478150e75/lib/word_counter.rb#L26
			// https://github.com/otwcode/otwarchive/blob/943f585818005be8df269d84ca454af478150e75/lib/word_counter.rb#L30C9-L31C95
			// Has not been tested on non-English works, feedback would be appreciated
			// const word_count_regex = /\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Thai}|((?!\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Thai})[\p{Letter}\p{Mark}\p{Number}\p{Connector_Punctuation}])+/gu;

			// Add support for most Unicode scripts supported in Regular Expressions
			// Vanilla AO3 compliant script_list:
			// const script_list = [`Han`, `Hiragana`, `Katakana`, `Thai`];
			// Full script_list:
			const script_list = [`Arabic`, `Armenian`, `Balinese`, `Bengali`, `Bopomofo`, `Braille`, `Buginese`, `Buhid`, `Canadian_Aboriginal`, `Carian`, `Cham`, `Cherokee`, `Common`, `Coptic`, `Cuneiform`, `Cypriot`, `Cyrillic`, `Deseret`, `Devanagari`, `Ethiopic`, `Georgian`, `Glagolitic`, `Gothic`, `Greek`, `Gujarati`, `Gurmukhi`, `Han`, `Hangul`, `Hanunoo`, `Hebrew`, `Hiragana`, `Inherited`, `Kannada`, `Katakana`, `Kayah_Li`, `Kharoshthi`, `Khmer`, `Lao`, `Latin`, `Lepcha`, `Limbu`, `Linear_B`, `Lycian`, `Lydian`, `Malayalam`, `Mongolian`, `Myanmar`, `New_Tai_Lue`, `Nko`, `Ogham`, `Ol_Chiki`, `Old_Italic`, `Old_Persian`, `Oriya`, `Osmanya`, `Phags_Pa`, `Phoenician`, `Rejang`, `Runic`, `Saurashtra`, `Shavian`, `Sinhala`, `Sundanese`, `Syloti_Nagri`, `Syriac`, `Tagalog`, `Tagbanwa`, `Tai_Le`, `Tamil`, `Telugu`, `Thaana`, `Thai`, `Tibetan`, `Tifinagh`, `Ugaritic`, `Vai`, `Yi`];
			// Excludes the Unicode scripts "Common" and "Latin" because that messes with the counting somehow
			// Exclude "Inherited" just to be safe
			const script_exclude_list = [`Common`, `Latin`, `Inherited`];
			const word_count_regex = new RegExp((function () {
				// Switch from using alternations in a group (e.g. (a|b|c)) to a character class (e.g. [abc]) for performance reasons (https://stackoverflow.com/a/27791811/11750206)
				const regex_scripts = script_list.filter((elm) => !script_exclude_list.includes(elm)).map((elm) => `\\p{Script=${elm}}`).join(``);
				const full_regex_str = `[${regex_scripts}]|((?![${regex_scripts}])[\\p{Letter}\\p{Mark}\\p{Number}\\p{Connector_Punctuation}])+`;
				return full_regex_str;
			})(), `gv`);

			// Count the number of words
			// Counting method from: https://stackoverflow.com/a/76673564/11750206, https://stackoverflow.com/a/69486719/11750206, and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll
			// Regex substitutions from: https://github.com/otwcode/otwarchive/blob/943f585818005be8df269d84ca454af478150e75/lib/word_counter.rb#L30C33-L30C68
			const word_count_arr = Array.from(chapter_text.replaceAll(/--/g, `—`).replaceAll(/['’‘-]/g, ``).matchAll(word_count_regex), (m) => m[0]);
			const word_count_int = word_count_arr.length;

			// Format the integer number to a thousands separated string (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat)
			const word_count_str = new Intl.NumberFormat({ style: `decimal` }).format(word_count_int);

			return word_count_str;
		})();

		console.log(`Word Count: ${word_count} words`);

		// Create element with the text "Words in Chapter"
		const chap_word_count_text = Object.assign(document.createElement(`dt`), {
			id: `chapter_words_label`,
			className: `chapter_words`,
			textContent: `Words in Chapter:`
		});

		// Create element with the word count of the chapter
		const chap_word_count_num = Object.assign(document.createElement(`dd`), {
			id: `chapter_words_number`,
			className: `chapter_words`,
			textContent: word_count
		});

		// Get the element where the stats are displayed
		const stats_elem = document.querySelector(`#main dl.work.meta.group dl.stats`);

		// Append the created elements after the element containing the total word count of the fic
		stats_elem.querySelector(`dd.words`).after(chap_word_count_text, chap_word_count_num);
	}
})();