ColorCodedLyrics Add Name Titles To Colored Lyric Lines

Attempts to automatically title color-coded lyric spans on colorcodedlyrics.com for convenience. May not work on all pages due to small HTML structure variations.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         ColorCodedLyrics Add Name Titles To Colored Lyric Lines
// @namespace    http://tampermonkey.net/
// @version      1.1.1
// @description  Attempts to automatically title color-coded lyric spans on colorcodedlyrics.com for convenience. May not work on all pages due to small HTML structure variations.
// @author       You
// @match        https://colorcodedlyrics.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=colorcodedlyrics.com
// @grant        none
// @license      MIT
// ==/UserScript==

/*The website colorcodedlyrics.com color codes k-pop lyrics based on who's singing them. This script tries to find the member-color key and grab the information. Then, it iterates over each colored lyric span and checks its color. If the span's color is recognized as corresponding to one of the members/the singer, it sets the span's "title" attribute to the name of the member the color corresponds to.
This script may not work on all colorcodedlyrics.com pages due to small variations in the HTML layouts of pages potentially preventing the script from finding the key or lyrics.
Most of this script is color processing code I copied from one of my other projects, because sometimes the site specifies the key's color in "rgb()" format but colors the lyrics in "#rrggbb" (hex triplet) format, and normalization is needed to compare the colors.*/

(function() {
    'use strict';

    //This color processing function was copied from R74n's sandboxels.r74n.com
    function hexToRGB(hex) {
        var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : null;
    }
    function RGBToHex(rgb) {
        var r = parseInt(rgb.r||rgb[0]);
        var g = parseInt(rgb.g||rgb[1]);
        var b = parseInt(rgb.b||rgb[2]);
        return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
    }
    //End

	//start of sandboxels "a_mod_by_alice.js" mod color conversion and processing code
    function englishFormatList(thingsArrayIn) {
        var thingsArray = thingsArrayIn;
        var amount = thingsArray.length;
        if(amount == 1) {
            return thingsArray[0]
        } else if(amount == 2) {
            return thingsArray.join(" and ")
        } else {
            var lastItem = thingsArray[thingsArray.length - 1];
            thingsArray[thingsArray.length - 1] = "and " + lastItem;
            return thingsArray.join(", ")
        };
    };

    //Limit number to [min, max]
	function bound(number,lowerBound,upperBound) {
		return Math.min(upperBound,Math.max(lowerBound,number));
	};

	//Emergency color wrapper
	function rgbColorBound(color) {
		return bound(color,0,255);
	};

    function rgbStringToUnvalidatedObject(string) { //turns rgb() to {r,g,b} with no bounds checking
		//console.log("Splitting string into object");
		string = string.split(",");
		var red = parseFloat(string[0].substring(4));
		var green = parseFloat(string[1]);
		var blue = parseFloat(string[2].slice(0,-1));
		//console.log("String split: outputs " + red + ", " + green + ", " + blue + ".");
		return {r: red, g: green, b: blue};
	};

	function rgbStringToObject(string,doRounding,doBounding) { //turns rgb() to {r,g,b}
		doRounding = doRounding ?? true;
		doBounding = doBounding ?? true;
		//console.log(`rgbStringToObject: ${string}`);
			//console.log("Splitting string into object");
		if( !(string.startsWith("rgb(")) || !(string.endsWith(")")) ) {
			throw new Error('Color must start with "rgb(" and end with ")"');
		};
		var red,green,blue;
		[red,green,blue] = string.match(/[0-9\-.]+/g).slice(0,3).map(x => parseFloat(x));
			//console.log(`Colors loaded (${red}, ${green}, ${blue})`);
		//NaN checking
			var redNaN = isNaN(red);
			var greenNaN = isNaN(green);
			var blueNaN = isNaN(blue);
			var NanErrorString = "One or more colors are NaN:"
			if(redNaN) { NanErrorString += " red" };
			if(greenNaN) { NanErrorString += " green" };
			if(blueNaN) { NanErrorString += " blue" };
			if(redNaN || greenNaN || blueNaN) { throw new Error(NanErrorString) };
		if(doRounding) {
			red = Math.round(red);
			green = Math.round(green);
			blue = Math.round(blue);
				//console.log(`Colors rounded to (${red}, ${green}, ${blue})`);
		};
		if(doBounding) {
			red = bound(red,0,255)
			green = bound(green,0,255)
			blue = bound(blue,0,255)
				//console.log(`Colors bounded to (${red}, ${green}, ${blue})`);
		};
			//console.log("String split: outputs " + red + ", " + green + ", " + blue + ".");
		return {r: red, g: green, b: blue};
	};

	function hslColorStringToObject(color) {
		if(!color.startsWith("hsl(") || !color.endsWith(")")) {
			throw new Error(`The color ${color} is not a valid hsl() color`)
		};
		var colorTempArray = color.split(",").map(x => x.trim())
		if(colorTempArray.length !== 3) {
			throw new Error(`The color ${color} is not a valid hsl() color`)
		};
		if(!colorTempArray[1].endsWith("%")) { console.log(`hslColorStringToObject: Saturation in color ${color} was missing a %`); colorTempArray[1] += "%"; }
		if(!colorTempArray[2].endsWith("%)")) { console.log(`hslColorStringToObject: Lightness in color ${color} was missing a %`); colorTempArray[2] = [colorTempArray[2].slice(0, colorTempArray[2].length - 1), "%", colorTempArray[2].slice(colorTempArray[2].length - 1)].join(''); }
		var hue = parseFloat(colorTempArray[0].substring(4));
		var saturation = parseFloat(colorTempArray[1].slice(0,-1))
		var lightness = parseFloat(colorTempArray[2].slice(0,-2));
		//NaN checking
			var hueNaN,saturationNaN,lightnessNaN;
			isNaN(hue) ? hueNaN = true : hueNaN = false;
			isNaN(saturation) ? saturationNaN = true : saturationNaN = false;
			isNaN(lightness) ? lightnessNaN = true : lightnessNaN = false;
			var NanErrorString = "One or more colors are NaN:"
			if(hueNaN) { NanErrorString += " hue" };
			if(saturationNaN) { NanErrorString += " saturation" };
			if(lightnessNaN) { NanErrorString += " lightness" };
			if(hueNaN || saturationNaN || lightnessNaN) { throw new Error(NanErrorString) };
		return {h: hue, s: saturation, l: lightness};
	};

	function rgbToHex(color) {
		//console.log(`rgbToHex called on ${typeof(color) === "object" ? JSON.stringify(color) : color}`);
		if(typeof(color) == "object") { //Expects object like "{r: 172, g: 11, b: 34}"
			var red = color.r;
			var green = color.g;
			var blue = color.b;
				//console.log(`Colors loaded (${red}, ${green}, ${blue})`);
			red = Math.round(red);
			green = Math.round(green);
			blue = Math.round(blue);
				//console.log(`Colors rounded to (${red}, ${green}, ${blue})`);
			red = bound(red,0,255)
			green = bound(green,0,255)
			blue = bound(blue,0,255)
				//console.log(`Colors bounded to (${red}, ${green}, ${blue})`);
			red = red.toString(16);
			green = green.toString(16);
			blue = blue.toString(16);
				//console.log(`Colors converted to (0x${red}, 0x${green}, 0x${blue})`);
			//console.log("Padding R");
			while(red.length < 2) {
				red = "0" + red;
			};
			//console.log("Padding G");
			while(green.length < 2) {
				green = "0" + green;
			};
			//console.log("Padding B");
			while(blue.length < 2) {
				blue = "0" + blue;
			};
				//console.log(`Colors padded to (0x${red}, 0x${green}, 0x${blue}), concatenating...`);
			return "#" + red + green + blue;
		} else if(typeof(color) == "string") { //Expects string like "rgb(20,137,4)". Also doesn't round properly for some reason...
				//console.log("Splitting string")
			color = rgbStringToUnvalidatedObject(color);
			red = color.r;
			green = color.g;
			blue = color.b;
				//console.log(`Colors loaded (${red}, ${green}, ${blue})`);
			red = Math.round(red);
			green = Math.round(green);
			blue = Math.round(blue);
				//console.log(`Colors rounded to (${red}, ${green}, ${blue})`);
			red = bound(red,0,255)
			green = bound(green,0,255)
			blue = bound(blue,0,255)
				//console.log(`Colors bounded to (${red}, ${green}, ${blue})`);
			red = red.toString(16);
			green = green.toString(16);
			blue = blue.toString(16);
				//console.log(`Colors converted to (0x${red}, 0x${green}, 0x${blue})`);
			//console.log("Padding R");
			while(red.length < 2) {
				red = "0" + red;
			};
			//console.log("Padding G");
			while(green.length < 2) {
				green = "0" + green;
			};
			//console.log("Padding B");
			while(blue.length < 2) {
				blue = "0" + blue;
			};
				//console.log(`Colors padded to (0x${red}, 0x${green}, 0x${blue}), concatenating...`);
			return "#" + red + green + blue;
			} else {
			throw new Error(`Received invalid color: ${color}`);
		};
	};

	function linearBlendTwoColorObjects(color1,color2,weight1) { /*third argument is for color1 and expects a float from 0
																  to 1, where 0 means "all color2" and 1 means "all color1"*/
		weight1 = weight1 ?? 0.5;
		var w1 = Math.min(Math.max(weight1,0),1);
		var red1 = color1.r;
		var green1 = color1.g;
		var blue1 = color1.b;
		var red2 = color2.r;
		var green2 = color2.g;
		var blue2 = color2.b;
		var red3 = (red1 * w1) + (red2 * (1 - w1));
		var green3 = (green1 * w1) + (green2 * (1 - w1));
		var blue3 = (blue1 * w1) + (blue2 * (1 - w1));
		return {r: red3, g: green3, b: blue3};
	};

	function lightenColor(color,offset,outputType="rgb") {
		if(typeof(color) === "string") {
			if(color.length < 10) {
			//console.log(`detected as hex: ${color}`);
				//catch missing octothorpes
				if(!color.startsWith("#")) {
					color = "#" + color;
				};
			//console.log(`octothorpe checked: ${color}`);

				offset = parseFloat(offset);
				if(isNaN(offset)) {
					throw new Error("Offset is NaN");
				};

				var oldColor = color; //for error display
				color = hexToRGB(color);
				if(color === null) {
					throw new Error(`hexToRGB(color) was null (${oldColor}, maybe it's an invalid hex triplet?)`);
				};

			//console.log("converted color: " + JSON.stringify(color));
				var red = color.r + offset;
				var green = color.g + offset;
				var blue = color.b + offset;
			//console.log(`altered color: rgb(${red},${green},${blue})`);

				//rounding and bounding
				red = Math.round(red);
				green = Math.round(green);
				blue = Math.round(blue);
			//console.log(`rounded color: rgb(${red},${green},${blue})`);
				red = bound(red,0,255)
				green = bound(green,0,255)
				blue = bound(blue,0,255)
			//console.log(`bounded color: rgb(${red},${green},${blue})`);

				color = {r: red, g: green, b: blue};

				switch(outputType.toLowerCase()) {
					case "rgb":
						return `rgb(${red},${green},${blue})`;
						break;
					case "hex":
						return rgbToHex(color);
						break;
					case "json":
						return color;
						break;
					default:
						throw new Error("outputType must be \"rgb\", \"hex\", \"json\"");
				};
			} else {
				if(color.startsWith("rgb(")) {
					color = convertColorFormats(color,"json"); //object conversion
				//console.log(`color converted to object: ${JSON.stringify(color)}`);

					offset = parseFloat(offset);
					if(isNaN(offset)) {
						throw new Error("Offset is NaN");
					};

					var red = color.r + offset;
					var green = color.g + offset;
					var blue = color.b + offset;
				//console.log(`altered color: rgb(${red},${green},${blue})`);

					//rounding and bounding
					red = Math.round(red);
					green = Math.round(green);
					blue = Math.round(blue);
				//console.log(`rounded color: rgb(${red},${green},${blue})`);
					red = bound(red,0,255)
					green = bound(green,0,255)
					blue = bound(blue,0,255)
				//console.log(`bounded color: rgb(${red},${green},${blue})`);

					color = {r: red, g: green, b: blue};

					switch(outputType.toLowerCase()) {
						case "rgb":
							return `rgb(${red},${green},${blue})`;
							break;
						case "hex":
							return rgbToHex(color);
							break;
						case "json":
							return color;
							break;
						default:
							throw new Error("outputType must be \"rgb\", \"hex\", \"json\"");
					};
				} /*else if(color.startsWith("hsl")) {
					throw new Error("HSL is not implemented yet");
				}*/ else {
					throw new Error('Color must be of the type "rgb(red,green,blue)"'/* or "hsl(hue,saturation%,luminance%)"*/);
				};
			};
		} else if(typeof(color) === "object") {
			if(typeof(color.r) === "undefined" || typeof(color.g) === "undefined" || typeof(color.b) === "undefined") {
				throw new Error("Color must be of the form {r: red, g: green, b: blue}");
			};

		//console.log("received color: " + JSON.stringify(color));
			var red = color.r + offset;
			var green = color.g + offset;
			var blue = color.b + offset;
		//console.log(`altered color: rgb(${red},${green},${blue})`);

			//rounding and bounding
			red = Math.round(red);
			green = Math.round(green);
			blue = Math.round(blue);
		//console.log(`rounded color: rgb(${red},${green},${blue})`);
			red = bound(red,0,255)
			green = bound(green,0,255)
			blue = bound(blue,0,255)
		//console.log(`bounded color: rgb(${red},${green},${blue})`);

			color = {r: red, g: green, b: blue};

			switch(outputType.toLowerCase()) {
				case "rgb":
					return `rgb(${red},${green},${blue})`;
					break;
				case "hex":
					return rgbToHex(color);
					break;
				case "json":
					return color;
					break;
				default:
					throw new Error("outputType must be \"rgb\", \"hex\", \"json\"");
			};
		};
	};

	function rgbObjectToString(color,stripAlpha) {
		stripAlpha = stripAlpha ?? false;
		if(typeof(color) !== "object") {
			throw new Error("Input color is not an object");
		};
		var red = color.r;
		var green = color.g;
		var blue = color.b;
			//console.log(`Colors loaded (${red}, ${green}, ${blue})`);
		red = Math.round(red);
		green = Math.round(green);
		blue = Math.round(blue);
			//console.log(`Colors rounded to (${red}, ${green}, ${blue})`);
		red = bound(red,0,255)
		green = bound(green,0,255)
		blue = bound(blue,0,255)
			//console.log(`Colors bounded to (${red}, ${green}, ${blue})`);
		return `rgb(${red},${green},${blue})`
	};

	function convertColorFormats(color,outputType="rgb",stripAlpha) {
		stripAlpha = stripAlpha ?? false;
		if(typeof(color) === "undefined") {
			//console.log("Warning: An element has an undefined color. Unfortunately, due to how the code is structured, I can't say which one.");
			//color = "#FF00FF";
			throw new Error("Color is undefined!");
		};
		//console.log("Logged color for convertColorFormats: " + color);
		var oldColor = color;
		var bytes,r,g,b,a;
		if(typeof(color) === "string") {
			//Hex input case

			if(color.length < 10) {
				//a proper hex quadruplet is still shorter than the shortest proper rgb() string
				//console.log(`detected as hex: ${color}`);
					//catch missing octothorpes
					if(!color.startsWith("#")) {
						color = "#" + color;
					};
				//console.log(`octothorpe checked: ${color}`);

				if(oldColor.length < 6) {
					bytes = oldColor.toLowerCase().match(/[a-z0-9]/g).map(x => parseInt(x.concat(x),16));
				} else {
					bytes = oldColor.toLowerCase().match(/[a-z0-9]{2}/g).map(x => parseInt(x,16));
				};
				r = bytes[0];
				g = bytes[1];
				b = bytes[2];
				if(bytes.length > 3) {
					a = bytes[3] / 255;
				} else {
					a = null
				};
				if(stripAlpha) { a = null };
				//to JSON for ease of use
				color = {"r": r, "g": g, "b": b};
				if(typeof(a) == "number") { color["a"] = a };
			} else {
				//otherwise assume rgb() input
				bytes = color.match(/[\d\.]+/g);
				if(typeof(bytes?.map) == "undefined") {
					console.log(bytes);
					bytes = [255,0,255]
				} else {
					bytes = bytes.map(x => Number(x));
				};
				r = bytes[0];
				g = bytes[1];
				b = bytes[2];
				if(bytes.length > 3) {
					a = bytes[3];
					if(a > 1) {
						a /= 255
					}
				} else {
					a = null
				};
				if(stripAlpha) { a = null };
				//to JSON for ease of use
				color = {"r": r, "g": g, "b": b}
				if(typeof(a) == "number") { color["a"] = a };
			};
		} else if(Array.isArray(color)) {
			bytes = color;
			r = bytes[0];
			g = bytes[1];
			b = bytes[2];
			if(bytes.length > 3) {
				a = bytes[3];
				if(a > 1) {
					a /= 255
				}
			} else {
				a = null
			};
			if(stripAlpha) { a = null };
			//to JSON for ease of use
			color = {"r": r, "g": g, "b": b}
			if(typeof(a) == "number") { color["a"] = a };
		} else if(typeof(color) == "object") {
			//variable mappings only
			r = color.r;
			g = color.g;
			b = color.b;
			if(typeof(color.a) == "number") {
				a = color.a;
			} else {
				a = null
			};
			if(stripAlpha) { a = null }
		};
		//Colors are now objects

		switch(outputType.toLowerCase()) {
			case "rgb":
			case "rgba":
				var _r,_g,_b,_a;
				_r = r;
				_g = g;
				_b = b;
				if(typeof(a) == "number") { _a = a } else { _a = null };
				var values;
				if(stripAlpha || _a == null) {
					values = [_r,_g,_b];
				} else {
					values = [_r,_g,_b,_a];
				};
				for(var i = 0; i <= 2; i++) {
					values[i] = Math.round(values[i])
				};
				return (typeof(a) == "number" ? "rgba" : "rgb") + `(${values.join(",")})`
			case "hex":
				var _r,_g,_b,_a;
				_r = r;
				_g = g;
				_b = b;
				if(typeof(a) == "number") { _a = Math.round(a * 255) } else { _a = null };
				var bytesToBe;
				if(stripAlpha || _a == null) {
					bytesToBe = [_r,_g,_b];
				} else {
					bytesToBe = [_r,_g,_b,_a];
				};
				return "#" + bytesToBe.map(x => Math.round(x).toString(16).padStart(2,"0")).join("");
			case "json":
				return color;
			case "array":
				return Object.values(color);
				break;
			default:
				throw new Error("outputType must be \"rgb\", \"hex\", \"json\", or \"array\"");
		}
	};

	function rgbHexCatcher(color) {
		return convertColorFormats(color,"rgb");
	};

	function _rgbHexCatcher(color) {
		return convertColorFormats(color,"rgb");
	};

	//https://stackoverflow.com/questions/46432335/hex-to-hsl-convert-javascript
	function rgbStringToHSL(rgb,outputType="array") { //Originally a hex-to-HSL function, edited to take RGB and spit out an array
		//console.log("HSLing some RGBs");
		var result = rgbStringToUnvalidatedObject(rgb);

		var r = result.r;
		var g = result.g;
		var b = result.b;

		r /= 255, g /= 255, b /= 255;
		var max = Math.max(r, g, b), min = Math.min(r, g, b);
		var h, s, l = (max + min) / 2;

		if(max == min){
			h = s = 0; // achromatic
		} else {
			var d = max - min;
			s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
			switch(max) {
				case r: h = (g - b) / d + (g < b ? 6 : 0); break;
				case g: h = (b - r) / d + 2; break;
				case b: h = (r - g) / d + 4; break;
			}
			h /= 6;
		};

		s = s*100;
		s = Math.round(s);
		l = l*100;
		l = Math.round(l);
		h = Math.round(360*h);

		//var colorInHSL = 'hsl(' + h + ', ' + s + '%, ' + l + '%)';
		//Edit to return an array
		switch(outputType.toLowerCase()) {
			case "array":
				return [h,s,l];
				break;
			case "hsl":
				return `hsl(${h},${s}%,${l}%)`;
				break;
			case "json":
				return {h: h, s: s, l: l};
			default:
				throw new Error("outputType must be \"array\", \"hsl\", or \"json\"");
				break;
		};
		//console.log("HSL output "+ colorInHSL + ".");

	};

	function normalizeColorToHslObject(color,arrayType) {
		arrayType = arrayType ?? null;
		var ambiguousArrayError = "changeSaturation can't tell if the array input is supposed to be RGB or HSL. Please use an \"arrayType\" argument of \"rgb\" or \"hsl\".";
		var isHsl = false;
		if(Array.isArray(color)) {
			if(arrayType === null) {
				throw new Error(ambiguousArrayError);
			} else if(arrayType === "rgb") {
				color = `rgb(${color[0]},${color[1]},${color[2]})`;
				color = rgbStringToHSL(color,"json"); //rgb arr to hsl obj
			} else if(arrayType === "hsl") {
				color = {h: color[0], s: color[1], l: color[2]}; //hsl arr to hsl obj
			} else {
				throw new Error(ambiguousArrayError);
			};
		} else {
			//by this point, any array cases would have been handled, leaving just hex (rgb), json rgb, json hsl, string rgb, and string hsl
			if(typeof(color) === "string") {
				if(color.length < 10) { //detect hex: assume hex triplet if too short to be a well-formed rgb()
					if(!color.startsWith("#")) {
						color = "#" + color; //catch missing #
					};
					isHsl = false;
				};
				if(color.startsWith("rgb(")) { //detect rgb(): self-explanatory
					isHsl = false;
				};
				if(color.startsWith("hsl(")) { //detect hsl(): self-explanatory
					isHsl = true;
				};
			} else if(typeof(color) === "object") {
				if(typeof(color.r) !== "undefined") { //detect {r,g,b}: check for r key
					isHsl = false;
				};
				if(typeof(color.h) !== "undefined") { //detect {h,s,l}: check for h key
					isHsl = true;
				};
			};
			if(!isHsl) {
				color = convertColorFormats(color,"rgb"); //make any RGBs rgb()
				color = rgbStringToHSL(color,"json"); //make that rgb() an {h,s,l}
			} else { //by this point, it would have to either be a string or an object
				if(typeof(color) === "string") { //if it's a string
					color = hslColorStringToObject(color) //now it's an object
				};
			};
		};
		return color;
	};

	function convertHslObjects(color,outputType="rgb") {
		if(color == null) { console.error("convertHslObjects: Color is null"); color = {h: 300, s: 100, l: 50} };
		switch(outputType.toLowerCase()) {
			//RGB cases
			case "rgb":
				color = convertColorFormats(hslToHex(...Object.values(color)),"json"); //hsl to hex, hex to rgb_json, and rgb_json to rgb()
				return `rgb(${color.r},${color.g},${color.b})`;
				break;
			case "hex":
				color = hslToHex(...Object.values(color)); //hsl to hex
				return color;
				break;
			case "rgbjson":
			case "rgb-json":
			case "rgb_json":
				color = hexToRGB(hslToHex(...Object.values(color))); //hsl to hex and hex to rgb_json
				return color;
				break;
			case "rgbarray":
			case "rgb-array":
			case "rgb_array":
				color = hexToRGB(hslToHex(...Object.values(color))); //hsl to hex, hex to rgb_json, and rgb_json to rgb_array
				return [color.r, color.g, color.b];
				break;
			//HSL cases
			case "hsl":
				//note: color was previously converted to {h, s, l}
				return `hsl(${color.h},${color.s}%,${color.l}%)`;
				break;
			case "hsljson":
			case "hsl-json":
			case "hsl_json":
				return color;
				break;
			case "hslarray":
			case "hsl-array":
			case "hsl_array":
				return [color.h, color.s, color.l];
				break;
			default:
				throw new Error("outputType must be \"rgb\", \"hex\", \"rgb_json\", \"rgb_array\", \"hsl\", \"hsl_json\", or \"hsl_array\"");
		}
	};

	function colorToHsl(color,outputType="json") {
		if(typeof(color.h) == "number" && typeof(color.s) == "number" && typeof(color.l) == "number") {
			return color
		};
		color = convertColorFormats(color,"rgb");
		color = rgbStringToHSL(color,outputType);
		return color;
	};

	//https://stackoverflow.com/questions/36721830/convert-hsl-to-rgb-and-hex
	function hslToHex(h, s, l) { //h, s, l params to hex triplet
	  //console.log(`Hexing some HSLs (the HSLs are ${h},${s},${l})`)
	  s = bound(s,0,100); //limit to 0-100
	  l = bound(l,0,100);
	  l /= 100;
	  var a = s * Math.min(l, 1 - l) / 100;
	  var f = n => {
		var k = (n + h / 30) % 12;
		var color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
		return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
	  };
	  //console.log(`Hexed to #${f(0)}${f(8)}${f(4)}`)
	  return `#${f(0)}${f(8)}${f(4)}`;
	};
	//end of sandboxels "a_mod_by_alice.js" mod color conversion and processing code

    //The actual color-titling code
	var colorSpans = Array.from(document.querySelectorAll('div.entry-content p[style*=text-align] span[style^="color"], div.entry-content td[style*=text-align] span[style^="color"], div.entry-content p.has-text-align-center span[style^="color"]') ?? []);
    if(!colorSpans || colorSpans.length < 1) {
        console.warn("No member-color key found.");
        return false
    };

	var table = {};
    var tableText = [];
	for(var i = 0; i < colorSpans.length; i++) {
		var span = colorSpans[i];
		var name = span.textContent.replaceAll(",","").replaceAll(String.fromCodePoint(0xA0)," ").trim();
        tableText.push(`${name}\t${span.style.color.replaceAll(" ","")}`);
		var color = convertHslObjects(normalizeColorToHslObject(span.style.color),"hex");
		table[color] ??= name;
	};
    tableText = tableText.join("\n"); console.log(tableText);

	var coloredSpans = document.querySelectorAll('div.entry-content span[style^="color"]') ?? [];
    var unrecognizedColors = [];
    if(!coloredSpans || coloredSpans.length < 1) {
        console.warn("No colored lyrics found!");
        return false
    };
    for(var j = 0; j < coloredSpans.length; j++) {
		var _span = coloredSpans[j];
		var _color = convertHslObjects(normalizeColorToHslObject(_span.style.color),"hex");
		var colorCorrespondingName = table[_color];
		if(colorCorrespondingName) {
            _span.setAttribute("title",colorCorrespondingName)
        } else {
            if(!(unrecognizedColors.includes(_color))) {
                unrecognizedColors.push(_color);
            };
        };
	};
    if(unrecognizedColors.length > 0) {
        console.warn(`Unknown member/singer for colors ${englishFormatList(unrecognizedColors)}`);
    };
    return true
})();