🏠 Home 

SB/SV/QQ: Threadmark Word Counter

Display the word count of threadmarked forum posts in the header area of the post


安装此脚本?
// ==UserScript==
// @name           SB/SV/QQ: Threadmark Word Counter
// @namespace      https://github.com/w4tchdoge
// @version        1.1.1-20240618_161840
// @description    Display the word count of threadmarked forum posts in the header area of the post
// @author         w4tchdoge
// @homepage       https://github.com/w4tchdoge/MISC-UserScripts
// @match          *://forums.spacebattles.com/threads/*
// @match          *://forums.sufficientvelocity.com/threads/*
// @match          *://forum.questionablequesting.com/threads/*
// @run-at         document-idle
// @license        AGPL-3.0-or-later
// @history        1.1.1 — Log the sum of the word count of all threadmarks currently being displayed to the browser console
// @history        1.1.0 — Reworked `CSSRGBintoComponents()` to better handle cases where the color is in the format `color(srgb ...)`. Changed how lightness_increase works so that it's a negative value when `color-scheme` is `light` and zero when `color-scheme` is neither `light` nor `dark`. Reworked `phb_color` so that the string it outputs uses the newer space separated `rgb()` parameters (https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#syntax)
// @history        1.0.0 — Initial commit
// ==/UserScript==
(function () {
`use strict`;
const start_time = performance.now();
console.log(`
Initializing Threadmark Word Counter UserScript
———————————————–————————————————
Time since Start: ${performance.now() - start_time}ms`
);
// Conversion of the word counting Regular Expression used by AO3, with added 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`);
function WordCounter(string, word_count_regex_expr = word_count_regex) {
// 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(string.replaceAll(/--/g, `—`).replaceAll(/['’‘-]/g, ``).matchAll(word_count_regex_expr), (m) => m[0]);
const word_count_int = word_count_arr.length;
return word_count_int;
}
// Colour Functions for manipulating wc_borderColor
// Split the initial CSS colour string into components
function CSSRGBintoComponents(rgb_str) {
const match_regex = /^(rgb|color\(srgb(?=\s))/i;
const split_regex = /[^\d\.%]/i;
const regex_test = match_regex.test(rgb_str);
if (regex_test == false) {
throw new Error(`The parameter passed to CSSRGBintoComponents() is not an rgb()/rgba()/color(srgb ...) CSS colour string.`);
}
function CommonFilter(input_str, splt_rgx = split_regex) {
const [red, green, blue, alpha] = input_str.split(splt_rgx)
.filter(elm => {
try {
return ((!isNaN(parseFloat(elm)) && isFinite(elm)) || elm.includes(`%`));
} catch (error) {
return false;
}
});
return [red, green, blue, alpha];
}
const color_test = rgb_str.split(/[\s\(]/i).at(0).toLowerCase().includes(`color`);
if (color_test) {
const [comp_red, comp_green, comp_blue, comp_alpha] = CommonFilter(rgb_str)
.map((element, index) => {
if (Boolean(element) && index < 3) {
const output_str = parseInt(parseFloat(element) * 255).toString();
return output_str;
} else { return element.toString(); }
});
return [comp_red, comp_green, comp_blue, comp_alpha];
} else {
const [comp_red, comp_green, comp_blue, comp_alpha] = CommonFilter(rgb_str);
return [comp_red, comp_green, comp_blue, comp_alpha];
}
}
// Reference XYZ values ; Observer: 2° ; Illuminant: D65
const [ref_X, ref_Y, ref_Z] = [95.047, 100.000, 108.883];
// Converts RGB components into CIELAB
// Input is an array of RGB components in that order
// Outputs an array of CIE Lab components in that order
// from https://stackoverflow.com/a/73998199/11750206
function RGBtoLAB([red, green, blue]) {
const [var_R, var_G, var_B] =
[red, green, blue]
.map(n => n / 255)
.map(n =>
n > 0.0405
? ((n + 0.055) / 1.055) ** 2.4
: n / 12.92
)
.map(n => n * 100);
const [X, Y, Z] = [
var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805,
var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722,
var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
];
const [var_X, var_Y, var_Z] =
[X / ref_X, Y / ref_Y, Z / ref_Z]
.map(n =>
n > 0.008856
? n ** (1 / 3)
: (7.787 * n) + (16 / 116)
);
const [CIE_L, CIE_a, CIE_b] =
[
(116 * var_Y) - 16,
500 * (var_X - var_Y),
200 * (var_Y - var_Z)
];
return [CIE_L, CIE_a, CIE_b];
}
// Converts CIELAB components into RGB
// Input is an array of CIE Lab components in that order
// Outputs an array of RGB components in that order
// from https://stackoverflow.com/a/73998199/11750206
function LABtoRGB([CIE_L, CIE_a, CIE_b]) {
const
var_Y = (CIE_L + 16) / 116,
var_X = (CIE_a / 500) + var_Y,
var_Z = var_Y - (CIE_b / 200);
const [X, Y, Z] = (() => {
const [mid_X, mid_Y, mid_Z] = [var_X, var_Y, var_Z]
.map(n =>
n ** 3 > 0.008856
? n ** 3
: (n - 16 / 116) / 7.787
);
return [mid_X * ref_X, mid_Y * ref_Y, mid_Z * ref_Z];
})();
const [var_R, var_G, var_B] = [
X * 3.2406 + Y * -1.5372 + Z * -0.4986,
X * -0.9689 + Y * 1.8758 + Z * 0.0415,
X * 0.0557 + Y * -0.2040 + Z * 1.0570
];
const [R, G, B] = [var_R, var_G, var_B]
.map(n => n / 100)
.map(n =>
n > 0.0031308
? (1.055 * (n ** (1 / 2.4))) - 0.055
: 12.92 * n
)
.map(n => parseInt(n * 255));
return [R, G, B];
}
// Get array of threadmarked posts
const threadmarked_posts = Array.from(document.querySelectorAll(`article.hasThreadmark:has(.message-inner .message-cell--main)`));
// Time logging because why not
console.log(`
Starting Word Counting of Threadmarks
———————————————–————————————————
Time since Start: ${performance.now() - start_time}ms`
);
let word_count_sum = 0;
// Iterate on the array of threadmarked posts
threadmarked_posts.forEach(function (element, index, array) {
// Get the timestamp element which the Word count element will be put after
const tmrkd_post_timestamp = element.querySelector(`.message-cell--main .message-attribution-main`);
// Get the computed styles of the threadmark header element (where the threadmark name,category, and nav buttons are)
// This is to mimick the border style when making the word count element
// Also getting the threadmark title and category because it's right there why not
const [threadmark_title, threadmark_category, ph_styles] = (function () {
const tmrk_header = element.querySelector(`div.message-cell--threadmark-header`);
const
tmrk_title = tmrk_header.querySelector(`span.primary > span[id^="threadmark"]`).cloneNode(true).textContent.trim(),
tmrk_category = tmrk_header.querySelector(`span.primary > label[for^="threadmark"]`).cloneNode(true).textContent.trim();
const tmrk_header_styles = getComputedStyle(tmrk_header);
return [tmrk_title, tmrk_category, tmrk_header_styles];
})();
// Change how much higher the perceptual lightness of the word count border colour is
// const lightness_increase = 10;
// const lightness_increase = 6.75;
const lightness_increase = (() => {
const up_value = 6.75;
const site_color_scheme = getComputedStyle(document.querySelector(`body`)).getPropertyValue(`color-scheme`);
// if (site_color_scheme == `light`) {
// 	const output_num = -up_value;
// 	return output_num;
// } else {
// 	const output_num = up_value;
// 	return output_num;
// }
switch (site_color_scheme) {
case `light`:
return -up_value;
case `dark`:
return up_value;
default:
return 0;
}
})();
// Assign/Get/Calculate the border properties for the word count element border
const [wc_borderWidth, wc_borderStyle, wc_borderColor] = (function () {
const
// Manually set to 1px because that's what it's set to in the Threadmark header
// But computed style returns 0.740 recurring
phb_width = `1px`,
// Get the border style
phb_style = ph_styles.getPropertyValue(`border-bottom-style`),
// Calculated the border colour based on retrieved border colour and lightness_increase
phb_color = (() => {
const [initial_red, initial_green, initial_blue, initial_alpha] = CSSRGBintoComponents(ph_styles.getPropertyValue(`border-bottom-color`));
const [i_CIE_L, i_CIE_a, i_CIE_b] = RGBtoLAB([initial_red, initial_green, initial_blue]);
const [final_CIE_L, final_CIE_a, final_CIE_b] = [i_CIE_L + lightness_increase, i_CIE_a, i_CIE_b];
const [final_red, final_green, final_blue] = LABtoRGB([final_CIE_L, final_CIE_a, final_CIE_b]);
if (Boolean(initial_alpha)) {
const out_phb_color = `rgb(${final_red} ${final_green} ${final_blue} / ${initial_alpha})`;
return out_phb_color;
} else {
const out_phb_color = `rgb(${final_red} ${final_green} ${final_blue})`;
return out_phb_color;
}
})();
return [phb_width, phb_style, phb_color];
})();
// Get the actual text of the threadmark to be word counted
const threadmark_text = element.querySelector(`.message-inner .message-cell--main .message-content article.message-body .bbWrapper`).cloneNode(true).textContent;
// Calculate word count using WordCounter() and format it to a thousand-separated string
const word_count_int = parseInt(WordCounter(threadmark_text));
const word_count_str = new Intl.NumberFormat({ style: `decimal` }).format(word_count_int);
// Add to sum
word_count_sum += word_count_int;
// Threadmark info logging + Time logging
console.log(
`
Threadmark ${index + 1}
Threadmark Category: ${threadmark_category}
Threadmark Title: ${threadmark_title}
Word Count: ${word_count_str} words
———————————————–————————————————
Time since Start: ${performance.now() - start_time}ms`
);
// Create the word count element
const word_count_element = Object.assign(document.createElement(`ul`), {
id: `threadmark-word-count`,
className: `listInline`,
// style: `padding-left: 0.5em; margin-left: 0.5em; border-left: ${wc_borderWidth} ${wc_borderStyle} ${wc_borderColor};`,
style: `padding-left: 7.5px; margin-left: 7.5px; border-left: ${wc_borderWidth} ${wc_borderStyle} ${wc_borderColor};`,
innerHTML: (function () {
const element = Object.assign(document.createElement(`li`), {
className: `u-concealed`,
innerHTML: `Word Count: ${word_count_str} words`
});
return element.outerHTML;
})()
});
// Check if the word count element already exists
const wc_elem_check = element.querySelector(`#threadmark-word-count`);
if (Boolean(wc_elem_check)) {
// If yes, replace existing one with new one
wc_elem_check.replaceWith(word_count_element);
} else {
// If no, add a word count element
tmrkd_post_timestamp.after(word_count_element);
}
});
const word_count_sum_elm = (() => {
const out_elm = Object.assign(document.createElement(`div`), {
id: `CrW_Thrdmrk_Wrd_Cnt_Sum_hidden`,
});
out_elm.setAttribute(`data_word_count_sum`, word_count_sum);
return out_elm;
})();
document.querySelector(`body`).appendChild(word_count_sum_elm);
console.log(`
Sum of the word counts of the threadmarks on the current page:`, word_count_sum);
// More time logging wheeeeeeeeeeeeeeeeeeee
console.log(`
Completed Word Counting of Threadmarks
UserScript has completed running
———————————————–————————————————
Time since Start: ${performance.now() - start_time}ms`
);
})();