BP Funcs

Small script to be @require-d, providing useful functions and extensions I like to refularly refer to

目前為 2022-06-26 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/447081/1064792/BP%20Funcs.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name		BP Funcs
// @description	Small script to be @require-d, providing useful functions and extensions I like to refularly refer to
// @version		1.0.0
// @namespace	BP
// @author		Benjamin Philipp <dev [at - please don't spam] benjamin-philipp.com>
// ==/UserScript==

/*
	BP Funcs, as of 2022-06-26 19:18:49 (GMT +02:00)
*/

String.prototype.after = function(str, fromRight, returnAll){
	if(fromRight === undefined)
		fromRight = false;
	if(returnAll === undefined)
		returnAll = false;
	var os = this.indexOf(str);
	if(fromRight)
		os = this.lastIndexOf(str);
	if(os<0)
		return returnAll?this:"";
	return this.substring(os + str.length);
};
String.prototype.before = function(str, fromRight, returnAll){
	if(fromRight === undefined)
		fromRight = false;
	if(returnAll === undefined)
		returnAll = false;
	var os = this.indexOf(str);
	if(fromRight)
		os = this.lastIndexOf(str);
	if(os<0)
		return returnAll?this:"";
	return this.substr(0, os);
};
function bpMenu(){
	var r = {};
	r.obj = null;
	r.items = {};
	r.styles = {
		default: {
			colors : {
				background : "#fff",
				text : "#333",
				item : "#555",
				itemHover : "#46a",
				itemBack : "transparent",
				frame : "#fff"
			},
			paddingFrame : "10px",
			radiusFrame : "10px",
			fontSize : "18px"
		}
	};
	r.style = r.styles.default;
	r.setup = function(override=false){
		var head, script;
		head = document.getElementsByTagName("head")[0];
//		if(typeof $ !== "function"){
//			console.log("jQuery not available?\nTrying to insert & load...", typeof $);
//			script = document.createElement("script");
//			script.type = "text/javascript";
//			script.onload = function(){
//				r.setup();
//			};
//			script.src = "https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js";
//			head.appendChild(script);
//			return;
//		}
		if(typeof bpModal !== "function"){
			console.log("BP Modal not available?\nTrying to insert & load...", typeof bpModal);
			script = document.createElement("script");
			script.type = "text/javascript";
			script.onload = function(){
				r.setup();
			};
			script.src = "https://benjamin-philipp.com/js/gm/funcs.js?funcs=bpModal";
			head.appendChild(script);
			return;
		}
//		console.log("Setup BP Menu");
		if(override){
			$("body>#bpMenu").remove();
		}
		r.injectStyles(override);

		if($("body>#bpMenu").length <=0)
			$("body").append("<div id='bpMenu'><div class='inner'></div></div>");
		r.obj = $("body>#bpMenu");
	};
	r.injectStyles = function(override=false){
		if(override)
			$("#bpMenuStyle").remove();
		if($("#bpMenuStyle").length<=0)
			$("head").append(`<style id="bpMenuStyle">
			.bpbutton{
				padding: 5px;
				display: inline-block;
				background: #aaa;
				color: #333;
				cursor: pointer;
				font-weight: 600;
				line-height: 1em;
			}
			.bpbutton:hover, .bpbutton.on{
				background: #1c82fa;
				color: #fff;
			}
			
			#bpMenu{
				position: fixed;
				z-index: 99999;
				top: -50px;
				right: 0px;
				height: 70px;
				display: inline-block;
				transition: top 0.5s;
				padding: 0px 0px 10px;
			}
			#bpMenu .inner{
				display: inline-block;
				background-color: #fff;
				color: #aaa;
				padding: 0px 10px 5px;
				border-radius: 0 0 10px 10px;
				box-shadow: 0 0 10px rgba(0,0,0,0.5);
			}
			#bpMenu:hover{
				top: 0px;
			}
			#bpMenu .bp{
				display: inline-block;
				padding: 5px;
				font-size: ' + r.style.fontSize + ';
			}
			#bpMenu .bp.item{
				color: ' + r.style.colors.item + ';
				font-weight: bold;
				cursor: pointer;
			}
			#bpMenu .bp.item:hover{
				color: ' + r.style.colors.itemHover + ';
			}
			#bpMenu .bp+.bp{
				margin-left: 10px;
			}
			</style>`);
	};
	r.add = function(id, html, cb=null, title="", override=false, sel=""){
		let l = $("body>#bpMenu>.inner #" + id);
		let add = true;
		if(l.length >0){
			add = false;
			if(override){
				l.remove();
				add = true;
			}
		}
		if(add){
			if(title)
				title = " title='" + title + "'";
			$("body>#bpMenu .inner").append("<div id='" + id + "' class='bp" + (cb?" item":"") + "'" + title + ">" + html + "</div>");
			r.items[id] = $("#bpMenu #" + id);
			if(cb)
				$("#bpMenu #" + id).click(function(e){
					cb(e);
				});
		}
	};
	r.changeStyle = function(obj){
		mergeDeep(r.style, obj);
		r.injectStyles(true);
	};
	r.setup();
	return r;
}
//if("undefined" === typeof bpMenuHelper){ // jshint ignore:line
//	var bpMenuHelper = bpMenu(); // jshint ignore:line
//}
function dateString(date, format){
     if(date===undefined || date===null || date === "")
        date = new Date();
     if(format===undefined)
        format="YYYY-MM-DD HH:mm:SS";
     else if(format==="file")
        format="YYYY-MM-DD HH-mm-SS";
	date = new Date(date);
     var year = date.getUTCFullYear();
     var months = pad(date.getUTCMonth() + 1);
     var days =  pad(date.getUTCDate());
     var hours = pad(date.getUTCHours());
     var minutes = pad(date.getUTCMinutes());
     var seconds = pad(date.getUTCSeconds());
     return format.replace("YYYY", year)
        .replace("MM", months)
        .replace("DD", days)
        .replace("HH", hours)
        .replace("mm", minutes)
        .replace("SS", seconds);
}
function pad(num){
    var r = String(num);
    if(r.length<=1)
        r = "0" + r;
    return r;
}
var bpTitleFormats = {
	movies: [
		"[title] [[year]]",
		"[title] ([year])",
		"[title] - [year]"
	],
	series: [
		"[name] - Season [season] Episode [episode] - [title]",
		"[name] - Season [season] Episode [episode]",
		"[name] - S[lzseason]E[lzepisode] - [title]",
		"[name] - S[lzseason]E[lz3episode] - [title]"
	]
};

var bpMediaTitleRegex = {
	movies: [
		/(.+) \[(\d{4})\]$/mi,
		/(.+) \((\d{4})\)$/mi,
		/(.+) - (\d{4})$/mi,
	],
	series: [
		// /(.+?) ?-? (?:(?:season |S)?0?0?(\d+))? ?(?:episode |E|x)0*(\d+(?:-\d+)?)(?: ?[:-]? (.+))?/mi
		/(.+?) ?-? (?:(?:season |S)?0*(\d+))?(?: -|,)?\s*(?:episode |E|x)0*(\d+(?:-\d+)?)(?: ?[:-]? (.+))?/mi
	]
};
{//// Check RegEx against:
	// Some series name 1x12
	// Some series name 1x12 and a title
	// Some series name 1x12 - and a title
	// Some series name S01E12
	// Some series name S01E12 and a title
	// Some series name S01E12 - and a title
	// Some series name S01E12-13
	// Some series name S01E12-13 and a title
	// Some series name S01E12-13 - and a title
	// Some series name - 1x12
	// Some series name - 1x12 and a title
	// Some series name - 1x12 - and a title
	// Some series name - S01E12
	// Some series name - S01E12 and a title
	// Some series name - S01E12 - and a title
	// Some series name - S01E12-13
	// Some series name - S01E12-13 and a title
	// Some series name - S01E12-13 - and a title
	// Some series name Episode 12
	// Some series name Episode 12 and a title
	// Some series name Episode 12 - and a title
	// Some series name Episode 12-13
	// Some series name Episode 12-13 and a title
	// Some series name Episode 12-13 - and a title
	// Some series name Season 1 Episode 12
	// Some series name Season 1 Episode 12 and a title
	// Some series name Season 1 Episode 12 - and a title
	// Some series name Season 1 Episode 12-13
	// Some series name Season 1 Episode 12-13 and a title
	// Some series name Season 1 Episode 12-13 - and a title
	// Some series name - Episode 12
	// Some series name - Episode 12 and a title
	// Some series name - Episode 12 - and a title
	// Some series name - Episode 12-13
	// Some series name - Episode 12-13 and a title
	// Some series name - Episode 12-13 - and a title
	// Some series name - Season 1 Episode 12
	// Some series name - Season 1 Episode 12 and a title
	// Some series name - Season 1 Episode 12 - and a title
	// Some series name - Season 1 Episode 12-13
	// Some series name - Season 1 Episode 12-13 and a title
	// Some series name - Season 1 Episode 12-13 - and a title
	// Some series name - Season 1 Episode 12: and a title
	// Some series name - Season 1 Episode 12 : and a title
	// Some series name - Season 1 Episode 12-13: and a title
	// Some series name - Season 1 Episode 12-13 : and a title
	// Some series name - Season 1, Episode 12
	// Some series name - Season 1, Episode 12 and a title
	// Some series name - Season 1, Episode 12 - and a title
	// Some series name - Season 1, Episode 12-13
	// Some series name - Season 1, Episode 12-13 and a title
	// Some series name - Season 1, Episode 12-13 - and a title
	// Some series name - Season 1, Episode 12: and a title
	// Some series name - Season 1, Episode 12 : and a title
	// Some series name - Season 1, Episode 12-13: and a title
	// Some series name - Season 1 - Episode 12
	// Some series name - Season 1 - Episode 12 and a title
	// Some series name - Season 1 - Episode 12 - and a title
	// Some series name - Season 1 - Episode 12-13
	// Some series name - Season 1 - Episode 12-13 and a title
	// Some series name - Season 1 - Episode 12-13 - and a title
	// Some series name - Season 1 - Episode 12: and a title
	// Some series name - Season 1 - Episode 12 : and a title
	// Some series name - Season 1 - Episode 12-13: and a title
	// Some series name - Season 1 - Episode 12-13 : and a title
}

function guessMovieOrTV(title){
	var tit = title.replace(/[—–]/g, "-"); // em-dash, en-dash
	for(let rex of bpMediaTitleRegex.series){
		if(rex.test(tit))
			return "TV";
	}
	for(let rex of bpMediaTitleRegex.movies){
		if(rex.test(tit))
			return "Movie";
	}
	return false;
}

function formatMovieTV(tit, templateSeries, templateMovie){
	switch(guessMovieOrTV(tit)){
		case "TV":
			return formatEpisodeTitle(tit, templateSeries);
		case "Movie":
			return formatMovieTitle(tit, templateMovie);
		default:
			console.log("Could not identify TV or Movie title");
			return tit;
	}
}

function formatMovieTitle(tit, template){
	if(!template)
		template = bpTitleFormats.movies[0];
		
//	console.log("preferred format: " + template);
	var match = false;
	for(let rex of bpMediaTitleRegex.movies){
		match = tit.match(rex);
		if(match){
//			match = rex.exec(tit);
			console.log("title matches format " + rex.toString(), match);
			break;
		}
	}
	if(!match){
		console.log("Title format not recognized", tit);
		return tit;
	}
	var name = match[1];
	var year = match[2];
	
	tit = template.replace("[title]", name)
		.replace("[year]", year);
	console.log("formatted title:", tit);
	return tit;
}

function formatEpisodeTitle(tit, template){
	if(!template)
		template = bpTitleFormats.series[0];
		
//	console.log("preferred format: " + template);
	
	tit = tit.replace(/[—–]/g, "-"); // em-dash, en-dash
	var match = false;
	for(let rex of bpMediaTitleRegex.series){
		match = tit.match(rex);
		if(match){
//			match = rex.exec(tit);
			console.log("title matches format " + rex.toString(), match);
			break;
		}
	}
	if(!match){
		console.log("Title format not recognized", tit);
		return tit;
	}
	var name = match[1];
	var season = match[2];
	if(!season)
		season = 1;
	var episode = match[3];
	var title = (match.length>=5 && match[4] !== undefined)? match[4] : "";
	if((/(Episode #? ?\d+|S\d+ ?E\d+)/i).test(title))
		title = "";
//	console.log({"name" : name, "season" : season, "episode" : episode, "title" : title});
	
	
	if(title===""){
		template = template.replace(/ ?-? \[title\]/, "");
		console.log("no title:", template);
	}
	
	tit = template.replace("[name]", name)
		.replace("[season]", season)
		.replace("[episode]", episode)
		.replace(/\[lz(\d*)season]/i, function(_,p){
			if(p==="")
				p = 2;
			return lz(season, p);
		})
		.replace(/\[lz(\d*)episode]/i, function(_,p){
			if(p==="")
				p = 2;
			return lz(episode, p);
		})
		.replace("[title]", title);
	console.log("formatted title:", tit);
	return tit;
}

function sanitize(str){
	str = str.replace(/[\\"]/g, "-");
	str = str.replace(/\?/g, "");
	str = str.replace(/[\/:]/g, " - ");
	str = str.replace(/[\/:]/g, " - ");
	str = str.replace(/\s+-\s*(?:-+\s+)+/g, " - ");
	str = str.replace(/\s\s+/g, " ");
	return str;
}

function lz(num, places = 2){
	return ("0000" + num).slice(-places);
}

function toTitleCase(str, preserveCaps=false, preserveAllCaps=false){
	return str.replace(/\w[^\s_:-]*/g, function(txt){
		var rest = txt.substr(1);
		if(!preserveCaps){
			if(preserveAllCaps){
				if(txt.charAt(0) != txt.charAt(0).toUpperCase()|| rest != rest.toUpperCase())
					rest = rest.toLowerCase();
			}
			else
				rest = rest.toLowerCase();
		}
		return txt.charAt(0).toUpperCase() + rest;
	});
}
const mimeTypes = {
	".aac": "audio/aac",
	".abw": "application/x-abiword",
	".arc": "application/x-freearc",
	".avif": "image/avif",
	".avi": "video/x-msvideo",
	".azw": "application/vnd.amazon.ebook",
	".bin": "application/octet-stream",
	".bmp": "image/bmp",
	".bz": "application/x-bzip",
	".bz2": "application/x-bzip2",
	".cda": "application/x-cdf",
	".csh": "application/x-csh",
	".css": "text/css",
	".csv": "text/csv",
	".doc": "application/msword",
	".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
	".eot": "application/vnd.ms-fontobject",
	".epub": "application/epub+zip",
	".gz": "application/gzip",
	".gif": "image/gif",
	".htm": "text/html",
	".html": "text/html",
	".ico": "image/vnd.microsoft.icon",
	".ics": "text/calendar",
	".jar": "application/java-archive",
	".jpeg.jpg": "image/jpeg",
	".js": "text/javascript",
	".json": "application/json",
	".jsonld": "application/ld+json",
	".mid.midi": "audio/midi",
	".mjs": "text/javascript",
	".mp3": "audio/mpeg",
	".mp4": "video/mp4",
	".mpeg": "video/mpeg",
	".mpkg": "application/vnd.apple.installer+xml",
	".odp": "application/vnd.oasis.opendocument.presentation",
	".ods": "application/vnd.oasis.opendocument.spreadsheet",
	".odt": "application/vnd.oasis.opendocument.text",
	".oga": "audio/ogg",
	".ogv": "video/ogg",
	".ogx": "application/ogg",
	".opus": "audio/opus",
	".otf": "font/otf",
	".png": "image/png",
	".pdf": "application/pdf",
	".php": "application/x-httpd-php",
	".ppt": "application/vnd.ms-powerpoint",
	".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
	".rar": "application/vnd.rar",
	".rtf": "application/rtf",
	".sh": "application/x-sh",
	".svg": "image/svg+xml",
	".swf": "application/x-shockwave-flash",
	".tar": "application/x-tar",
	".tif": "image/tiff",
	".tiff": "image/tiff",
	".ts": "video/mp2t",
	".ttf": "font/ttf",
	".txt": "text/plain",
	".vsd": "application/vnd.visio",
	".wav": "audio/wav",
	".weba": "audio/webm",
	".webm": "video/webm",
	".webp": "image/webp",
	".woff": "font/woff",
	".woff2": "font/woff2",
	".xhtml": "application/xhtml+xml",
	".xls": "application/vnd.ms-excel",
	".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
	".xml": "application/xml",
	".xul": "application/vnd.mozilla.xul+xml",
	".zip": "application/zip",
	".3gp": "video/3gpp",
	".3g2": "video/3gpp2",
	".7z": "application/x-7z-compressed"
};

function getContentTypeByExtension(ext){
	var defaultType = "application/octet-stream";
	if(typeof ext != "string")
		return console.warn('[getContentTypeByExtension] Invalid type supplied, please supply string', ext) || defaultType;
	if(ext.indexOf(".")<0)
		ext = "." + ext;
	else
		ext = ext.replace(/^.*(?=[.]\w+$)/, '');
	var mime = mimeTypes[ext.toLowerCase()];
	if(!mime)
		return console.warn('[getContentTypeByExtension] Failed to resolve for content name: %s', ext) || defaultType;
	return mime;
}
function getParam(s){
	return getParamFromString(location.href, s);
}

function getParamFromString(u, s){
	var url = new URL(u);
	return url.searchParams.get(s);
}
function getSelectedElements(){
	var allSelected = [];
	try{
		var selection = window.getSelection();
		var range = selection.getRangeAt(0);
		if(range.startOffset == range.endOffset)
			return allSelected;
		var cont = range.commonAncestorContainer;
		if(!cont){
//			console.log("no parent container?");
			return range.startContainer;
		}
		if(!cont.nodeName || cont.nodeName == "#text" || !cont.getElementsByTagName){
			var p = cont.parentElement;
//			console.log("weird container or text node; return parent", cont, p);
			if(!p){
//				console.log("actually, never mind; has no parent. Return element instead");
				return [cont];
			}
			return [p];
		}
		var allWithinRangeParent = cont.getElementsByTagName("*");

		for (var i=0, el; el = allWithinRangeParent[i]; i++){ // jshint ignore:line
			// The second parameter says to include the element 
			// even if it's not fully selected
			if (selection.containsNode(el, true))
				allSelected.push(el);
		}
	}catch(e){
		console.log(e);
	}
	return allSelected;
}
function htmlEntities(str){
	return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
		return '&#' + i.charCodeAt(0) + ';';
	});
}
function isNativeFunction(value) {
	// Used to resolve the internal `[[Class]]` of values
	var toString = Object.prototype.toString;

	// Used to resolve the decompiled source of functions
	var fnToString = Function.prototype.toString;

	// Used to detect host constructors (Safari > 4; really typed array specific)
	var reHostCtor = /^\[object .+?Constructor\]$/;

	// Compile a regexp using a common native method as a template.
	// We chose `Object#toString` because there's a good chance it is not being mucked with.
	var reNative = RegExp('^' +
		// Coerce `Object#toString` to a string
		String(toString)
		// Escape any special regexp characters
		.replace(/[.*+?^${}()|[\]\/\\]/g, '\\$&')
		// Replace mentions of `toString` with `.*?` to keep the template generic.
		// Replace thing like `for ...` to support environments like Rhino which add extra info
		// such as method arity.
		.replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
	);
	var type = typeof value;
	return type == 'function'
		// Use `Function#toString` to bypass the value's own `toString` method
		// and avoid being faked out.
		? // jshint ignore:line
		reNative.test(fnToString.call(value))
		// Fallback to a host object check because some environments will represent
		// things like typed arrays as DOM methods which may not conform to the
		// normal native pattern.
		:
		(value && type == 'object' && reHostCtor.test(toString.call(value))) || false;
}
function jqExtend(jQ=false){
	if(jQ || ("undefined" != typeof(jQuery) && "undefined" != typeof(jQuery.fn)) || ("undefined" != typeof($) && "undefined" != typeof($.fn))){
		var fn;
		if("undefined" != typeof(jQ) && "undefined" != typeof(jQ.fn))
			fn = jQ.fn;
		else if("undefined" != typeof(jQuery) && "undefined" != typeof(jQuery.fn))
			fn = jQuery.fn;
		else
			fn = $.fn;
		fn.selectText = function(){
			var doc = document;
			for(var i = 0; i<this.length; i++){
				var element = this[i];
				var range;
				if (doc.body.createTextRange){
					range = document.body.createTextRange();
					range.moveToElementText(element);
					range.select();
				} else if (window.getSelection){
					var selection = window.getSelection();
					range = document.createRange();
					range.selectNodeContents(element);
					selection.removeAllRanges();
					selection.addRange(range);
				}
			}
		};

		fn.fare = function(){
			$(this).fadeOut(function(){
				$(this).remove();
			});
		};

		fn.textOnly = function(trim=true){
			var c = this.clone();
			c.children().remove();
			if(trim)
				return c.text().trim();
			return c.text();
		};
	}
	else
	console.log("no jQuery, no extensions :(");
}
jqExtend();
class BPLogger {
	constructor(name = false, prefix = false, debugging = false, level = 1){
		if(!name && typeof GM_info !== "undefined")
			name = GM_info.script.name;
		
		if(prefix === false && name)
			prefix = name;
		
		if(!name)
			name = "Logger";
		
		this.name = name;
		this.prefix = prefix;
		this.debugging = debugging;
		this.level = level;

		this.colors = {
			default: [180, 100],
			warn: [60, 100],
			error: [0, 100],
			success: [150, 100]
		};
		this.history = [];
		this.keepHistory = false;

		if (typeof name == "object"){
			Object.assign(this, name);
		}
		return this;
	}

	writeLog(args, type = "default", level = 1){
		if (this.keepHistory)
			this.history.push([Date.now(), type, level, args]);
		if(this.prefix)
			args = ["%c" + this.prefix + ":", `color: hsl(${this.colors[type][0]},${this.colors[type][1]}%,80%); background-color: hsl(${this.colors[type][0]},${this.colors[type][1]}%,15%); font-weight: 900!important`, ...args];
		if (this.debugging)
			args = [...args, new Error().stack.replace(/^\s*(Error|Stack trace):?\n/gi, "").replace(/^([^\n]*\n)/, "\n")];
		
		if(["warn", "error"].includes(type))
			console[type](...args);
		else
			console.log(...args);
	}
	log(...args){
		this.writeLog(args);
	}
	warn(...args){
		this.writeLog(args, "warn");
	}
	error(...args){
		this.writeLog(args, "error");
	}
	success(...args){
		this.writeLog(args, "success");
	}
}

function BPLogger_default(...args){
	if(args.length<=0)
		args = "";
	var logger = new BPLogger(args);
	log = function(...args){
		logger.log(...args);
	};
	warn = function(...args){
		logger.warn(...args);
	};
	error = function(...args){
		logger.error(...args);
	};
	success = function(...args){
		logger.success(...args);
	};
	return logger;
}
String.prototype.matches = function(rex){
	if(!(rex instanceof RegExp))
		return log("Not a regular Expression:", rex);
	return rex.exec(this);
};

function mergeDeep(target, source, mutate=true){
	let output = mutate ? target : Object.assign({}, target);
	if(typeof target == "object"){
		if(typeof source != "object")
			source = {source};
			
		Object.keys(source).forEach(key => {
			if(typeof source[key] == "object"){
				if(!(key in target))
					Object.assign(output, { [key]: source[key] });
				else
				output[key] = mergeDeep(target[key], source[key]);
			}else{
				Object.assign(output, { [key]: source[key] });
			}
		});
	}
	return output;
}
function bpModal(){
	
	var r = {};
	r.messages = [];
	r.count = 0;
	r.setup = function(){
//		if(typeof $ !== "function"){
//			console.log("jQuery not available?\nTrying to insert & load...", typeof $);
//			var head = document.getElementsByTagName("head")[0];
//			var script = document.createElement("script");
//			script.type = "text/javascript";
//			script.onload = function(){
//				r.setup();
//			};
//			script.id="bpJQ";
//			script.src = "https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js";
//			head.appendChild(script);
//			return;
//		}
//		console.log("Actual setup with jQuery");
		if($("head #bpModalStyle").length<=0){
			$("head").append(`<style id="bpModalStyle">
				#messageoverlays{
					position:fixed;
					top:20vh;
					z-index:100;
					text-align:left;
					margin: 0 auto;
					left:50%;
					transform: translate(-50%, 0px);
				}

				#messageoverlays>.table{
					margin: 0px auto;
				}

				#messageoverlays .msg{
					display:inline-block;
					width:auto;
					margin: 5px auto;
					position:relative;
					padding: 10px;
					box-sizing: border-box;
					border-radius: 5px;
				}
				#messageoverlays .msg{
					border: 1px solid #666;
					background-color: #ddd;
					color: #333;
				}
				#messageoverlays .msg.dark,
				.dark #messageoverlays .msg,
				#messageoverlays.dark .msg{
					border: 1px solid #ccc;
					background-color: #333;
					color: #ccc;
				}

				#messageoverlays .msg.error{
					border-color: #a10;
					color: #710;
					background-color: #ffcabf;
				}
				#messageoverlays .msg.dark.error,
				.dark #messageoverlays .msg.error,
				#messageoverlays.dark .msg.error{
					background-color: #300;
					color: #b66;
					border: 1px solid #b66;
				}

				#messageoverlays .msg.warn{
					border-color: #dd0;
					color: #bb0;
					background-color: #fec;
				}
				#messageoverlays .msg.dark.warn,
				.dark #messageoverlays .msg.warn,
				#messageoverlays.dark .msg.warn{
					background-color: #330;
					color: #bb6;
					border: 1px solid #bb6;
				}

				#messageoverlays .msg.success{
					border-color: #190;
					color: #070;
					background-color: #bf9;
				}
				#messageoverlays .msg.dark.success,
				.dark #messageoverlays .msg.success,
				#messageoverlays.dark .msg.success{
					background-color:#030;
					color:#6b6;
					border:1px solid #6b6;
				}

				.closebutton{
					font-weight: 900;
					font-size: 12px;
					cursor: pointer;
					z-index: 20;
					opacity: 0.75;
					color: #fff;
					background-color: #a10;
					padding: 0 5px 1px;
					border-radius: 100%;
					position: absolute;
					right: -5px;
					top: -2px;
					line-height: 16px;
				}

				.closebutton:hover,
				.bpModback .modclose:hover{
					opacity: 1;
				}


				.bpModback{
					position:fixed;
					width:100%;
					height:100%;
					display:table;
					left:0;
					top:0;
					z-index:99000;
				}
				
				.bpModback.tint{
					background-color:rgba(0,0,0,0.5);
				}
				
				.bpModback.nomodal{
					display:block;
					width: auto;
					height: auto;
					left: 50%;
					top: 20px;
					transform: translateX(-50%);
				}
				
				.bpModback .modcent{
					display:table-cell;
					vertical-align:middle;
					height:100%;
					max-height:100%;
					min-height:100%;
				}
				
				.bpModback.nomodal .modcent{
					height:auto;
					max-height:auto;
				}
				
				.bpModback .modtable{
					display:table;
					margin:auto;
					position:relative;
					left:0;
				}
				
				.bpModback .modframe{
					border-radius: 6px;
					border:10px solid #fff;
					display:block;
					background-color: #fff;
					box-shadow: 0 0 20px rgba(0,0,0,0.5);
					max-height: 90vh!important;
					max-width: 90vw!important;
					overflow-y:auto;
				}
				
				.bpModback .modclose{
					display:block;
					background-color: #000;
					color: #fff;
					opacity:0.7;
					position:absolute;
					right:-12px;
					top:-12px;
					-webkit-border-radius: 20px;
					-moz-border-radius: 20px;
					border-radius: 20px;
					border:4px solid #fff;
					font-weight:900;
					font-size:12pt;
					padding:0px 7px;
					cursor:pointer;
					z-index:400;
				}
				
				.bpModback .modbox{
					position:relative;
					display: table;
					padding:20px 20px 10px;
					color:#666;
					overflow:hidden;
					display:block;
					/*text-align:center;*/
				}
				.bpModback .table{
					display:table;
				}
				
				.bpModback .tr{
					display:table-row;
				}
				
				.bpModback .td{
					display: table-cell;
				}
				#watch #player{
					height: 200px;
				}
				</style>`);
		}
	};
	r.msg = function(instr, id="", modal=true, callback=null){
		r.count++;
		if(!id){
			id = "bpmod" + r.count;
		}
		var noclose = false;
		if(typeof(modal)=="string" && modal == "noclose"){
			noclose = true;
			modal = true;
		}
		var m = {
			id : id,
			msg: instr,
			callback: callback,
			modal: modal,
			obj: $("<div class='bpModback " + (!!modal?"tint":"nomodal") + (noclose?" noclose":"") + "' id='" + id + "'><div class='tr'><div class='modcent'><div class='modtable'><div class='modclose'>X</div><div class='modframe'><div class='modbox'>" + instr + "</div></div></div></div></div></div>"),
			close: function(){
				this.obj.remove();
				delete r.messages[this.id];
				if(this.callback)
					this.callback(this);
			}
		};

		$("body").append(m.obj);
		
		$("#" + id + ":not('.noclose') .modcent").click(function(e){
			if(e.target == this)
				m.close(this);
		});
		$("#" + id + " .modclose").click(function(e){
			m.close(this);
		});
		r.messages[id] = m;
		return m;
	};
	r.close = function(el="all"){
		if(el=="all"){
			
		}
	};
	r.setup();
	return r;
}
//if("undefined" === typeof bpModHelper){ // jshint ignore:line
//	var bpModHelper = bpModal(); // jshint ignore:line
//}
function message(content, classname, id, expirein, closable){
	expirein = typeof expirein !== 'undefined' ? expirein : 0;
	if(closable===undefined)
		closable = true;
	var expires = expirein !== 0 ? true : false;
	if (id === undefined || id === ""){
		for (var i = 0; i < 512; i++){
			if ($(document).find("#message-" + i)[0] !== undefined){} else {
				this.id = "message-" + i;
				break;
			}
		}
	} else {
		this.id = id;
	}
	var fid = this.id;
	this.expire = function(){
		if (expirein > 0){
			if(this.extimer)
				window.clearTimeout(this.extimer);
			this.extimer = window.setTimeout(function(){
				$("#" + fid).fadeOut(function(){
					$("#" + fid).remove();
				});
			}, expirein);
		}
	};
	this.html = "<div id='" + this.id + "' class='table'><div class='msg " + classname + "'>" + content + (closable?"<div class='closebutton' id='c-" + this.id + "'>x</div>":"") + "</div></div>";
}

function overlaymessage(content, classname, id, expirein, closable){
	expirein = typeof expirein !== 'undefined' ? expirein : 5000;
	classname = classname || "hint";
	id = id || "";
	var curmes = new message(content, classname, id, expirein, closable);
	//console.log(curmes);
	if($("#messageoverlays").length<=0)
		$("body").append("<div id='messageoverlays'></div>");
	$("#messageoverlays").append(curmes.html);
	$(".msg .closebutton").off("click").on("click", function(){
		console.log("close", $(this).parent().parent());
		$(this).parent().parent().fare();
	});
	curmes.expire();
}

function msg(content, classname, id, expirein){
	overlaymessage(content, classname, id, expirein);
}

function msgbox(content, classname="", id=""){
	if (id === undefined || id === ""){
		for (var i = 0; i < 512; i++){
			if ($(document).find("#message-" + i)[0] !== undefined){} else {
				id = "message-" + i;
				break;
			}
		}
	} else {
		id = id;
	}
	return "<div id='" + id + "' class='msg " + classname + "'>" + content + "</div>";
}
String.prototype.rIndexOf = function(regex, startpos) {
    var indexOf = this.substring(startpos || 0).search(regex);
    return (indexOf >= 0) ? (indexOf + (startpos || 0)) : indexOf;
};
var regEsc = regEsc?regEsc : function(str) {
  return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};

String.prototype.replaceAll = function(str1, str2, insensitive)
{
    return this.replace(new RegExp(regEsc(str1),(insensitive?"gi":"g")),(typeof(str2)=="string")?str2.replace(/\$/g,"$$$$"):str2);
};
function scrollIntoView(object, offsetTop = 20, offsetLeft = 20){
	object = $(object);
	if(object.length<=0)
		return;
	object = object[0];
	var offset = $(object).offset();
	$('html, body').animate({
		scrollTop: offset.top - offsetTop,
		scrollLeft: offset.left - offsetLeft
	});
	object.scrollIntoView();
}
/* globals isElement, isNativeFunction, uneval */
function stringify(obj, forHTML, onlyOwnProperties, completeFunctions, level, maxLevel, skipEmpty){
	if(!level) level = 0;
	var r = "";
	if(obj===undefined) r = "[undefined]";
	else if(obj === null) r = "[null]";
	else if(obj === false) r = "FALSE";
	else if(obj === true) r = "TRUE";
	else if(obj==="") r = "[empty]";
	else if(typeof obj == "object"){
		var isDOMElement = isElement(obj);
		if(onlyOwnProperties === undefined) onlyOwnProperties = true;
		if(completeFunctions === undefined) completeFunctions = false;
		if(maxLevel === undefined) maxLevel = 5;
		if(skipEmpty === undefined) skipEmpty = false;
		
		r = "[object] ";
		var level_padding = "";
		var padString = "    ";
		for(var j = 0; j < level; j++) level_padding += padString;
		
		if(isDOMElement){
			r = "[DOMElement " + obj.nodeName + "] ";
			skipEmpty = true;
			completeFunctions = false;
		}
		
		if(level<maxLevel){
			r += "{\n";
			if(isDOMElement){
				r += level_padding + padString + "HTML => " + obj.outerHTML.replace(/\r?\n/g, "\\n").replace(/\s+/g, " ") + "\n";
			}
			for(var item in obj){
				try{
					var value = obj[item];
					if(onlyOwnProperties && obj.hasOwnProperty && !obj.hasOwnProperty(item) || isNativeFunction(value) || skipEmpty && (value===undefined || value === null || value===""))
						continue;
					
					if(typeof(value) == 'object'){
						r += level_padding + padString + "'" + item + "' => ";
						r += stringify(value, forHTML, onlyOwnProperties, completeFunctions, level+1, maxLevel, skipEmpty) + "\n";
					}else if(typeof(value) == 'undefined'){
						r += level_padding + padString + "'" + item + "' => [undefined]\n";
					}else{
						if(typeof(value.toString)=="function")
							value = value.toString();
						if(!completeFunctions){
							let m = value.match(/function\s*\(([^\)]*)\)\s*\{/i);
							if(m)
								value = "function(" + m[1] + ")";
						}
						r += level_padding + padString + "'" + item + ("' => \"" + value).replace(/\r?\n/g, "\\n").replace(/\s+/g, " ") + "\"\n";
					}
				}catch(e){
					console.log(e);
				}
			}
			r += level_padding + "}";
		}else
			r += "[Max depth of " + maxLevel + " exceeded]";
	}
	else if(typeof obj == "function"){
		if(typeof(obj.toString)=="function")
			r = obj.toString();
		else
			r = uneval(obj);
		if(!completeFunctions){
			let m = r.match(/function\s*\(([^\)]*)\)\s*\{/i);
			if(m)
				r = "function(" + m[1] + ")";
		}
	}
	else
		r = obj + "";
	
	if(level===0){
		if(!!forHTML){
			r = r.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
			r = "<pre>" + r + "</pre>";
		}
	}
	return r;
}
function isNode(o){
	return (
		typeof Node === "object" ? o instanceof Node : 
		o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string"
	);
}
/* globals HTMLDocument */
function isElement(o){
	return (
		((typeof HTMLElement === "object" && o instanceof HTMLElement) || (typeof Element === "object" && o instanceof Element) || (typeof HTMLDocument === "object" && o instanceof HTMLDocument))? true : //DOM2
		o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string"
	);
}
function stringifyWithFuncs(val, withTabs = true){
	return JSON.stringify(val, function(key, value){
		if (typeof value === 'function') {
			return value.toString();
		}
		return value;
	}, withTabs?"\t":" ");
}
function toText(str, maxBreaks=2){
//	if($ === undefined){
//		$ = jQuery = require( "jquery" )(new JSDOM("").window);
//	}
	var hack = "__break god dammit__";
	str = str.replace(/(<(?:br|hr) ?\/?>|<\/(?:p|li|div|td|h\d)>)/gi, hack + "$1");
	str = $("<div/>").append(str).text();
	var rex = new RegExp(hack, "gi");
	str = str.replace(rex, "\n");
	rex = new RegExp("(\\n{" + maxBreaks + "})\\n+", "g");
	str = str.replace(rex, "$1");
	return str.trim();
}
function trim(s, what="\\s"){
	var rex = new RegExp("^(?:[" + what + "])*((?:[\\r\\n]|.)*?)(?:[" + what + "])*$");
	var m = s.match(rex);
//	log(m);
	if(m !== null && m.length>=2)
		return m[1];
	return "";
}
function varToPretty(str){
	return str.replace(/(.+?)([A-Z])/g, "$1 $2").replace(/_|-/g, " ").replace(/\s\s+/g, " ").replace(/\b([a-z])/g, function(v,i){return v.toUpperCase();});
}
class eleWaiter{
	constructor(sel, cb, cbFail=null, findIn="document", delay=500, maxTries=50, alwaysOn=false, autoStart=true, debug = false){
		this.sel = "";
		this.cb = null;
		this.cbFail = null;
		this.findIn = "document";
		this.delay = 500;
		this.maxTries = 50;
		this.alwaysOn = false;
		this.autoStart = true;
		this.debug = false;
		this.colors = {
			label: [180,100],
			warn: [60,100],
			error: [0,100]
		};

		this.__running = false;
		this.__tries = 0;
		this.__timer = 0;
		this.__jqo = {};

		if(typeof sel == "object" && !(sel instanceof Array)){ // 2022-04-16 : Now allowing array of selectors
			// log("got object");
			Object.assign(this, sel);
		}
		else{
			this.sel = sel;
			this.cb = cb;
			if(cbFail!== undefined || cbFail!== null)
			this.cbFail = cbFail;
			if(findIn!== undefined || findIn!== null)
				this.findIn = findIn;
			this.delay = delay;
			this.maxTries = maxTries;
			this.alwaysOn = alwaysOn;
			this.autoStart = autoStart;
			this.debug = debug;
		}
		
		if(typeof this.sel == "string"){  // 2022-04-16 : Now allowing array of selectors
			this.sel = [this.sel];
		}
		
		if(this.debug){
			if(typeof this.debug == "string"){
				this.debug = {
					prefix: this.debug + " ",
					level: 1
				};
			}
			else if(typeof this.debug == "number"){
				this.debug = {
					prefix: "",
					level: this.debug
				};
			}
			else if(typeof this.debug == "object"){
				if(!this.debug.prefix)
					this.debug.prefix = "";
				else
					this.debug.prefix += " ";
			
				if(!this.debug.level)
					this.debug.level = 1;
			}
			else{
				this.debug = {
					prefix: "",
					level: 1
				};
			}
		}
//		this.log(this.cb);
		this.log(this);
		if(this.autoStart)
			this.__wait();
	}
	
	log(...args){
		if(!this.debug)
			return;
		
		if(typeof args == "object" && args.length>=2 && typeof args[args.length-1] == "string" && args[args.length-1].toLowerCase().indexOf("loglevel:")===0){
			var level = args[args.length-1].substr(9)*1;
			if(level>this.debug.level){
				return;
			}
			args.pop();
		}
		console.log(this.debug.prefix + "EleWaiter:", ...args);
	}

	start(){
		if(!this.__running){
			this.log("Start waiting", this.findIn, this.sel);
			this.__wait();
		}
	}
	stop(){
		clearTimeout(this.__timer);
		this.__running = false;
	}

	__wait(){
		if(this.findIn == "document" && !!document)
			this.findIn = document;
		
		this.__running = true;
		if(this.maxTries!=-1)
			this.__tries++;
		var triesLeft = this.alwaysOn?1:(this.maxTries - this.__tries);
		this.log("tries left:", triesLeft, "loglevel:3");
		var hasAll = true;
		this.__jqo = $();
		for(let sel of this.sel){
			var jqo = $(this.findIn).find(sel);

			if(jqo.length<=0){
				if(this.debug && this.debug.level>2 || !this.alwaysOn)
					this.log("Not found: " + sel, "in", this.findIn);
				if(triesLeft!==0){
					this.__timer = setTimeout(function(){this.__wait();}.bind(this), this.delay);
					if(this.alwaysOn)
						this.__result(false);
				}
				else
					this.__result(false);
				return;
			}
			else{
				this.__jqo = this.__jqo.add(jqo);
				this.log("Found something, is now:", this.__jqo, "loglevel:3");
			}
		}
		this.__result(this.__jqo);

		if(this.alwaysOn){
			this.log("Always on, repeat", "loglevel:3");
			this.__timer = setTimeout(function(){this.__wait();}.bind(this), this.delay);
		}
	}
	__result(success=false){
		if(!this.alwaysOn){
			this.__running = false;
			this.log("Result:", success, "loglevel:2");
		}else if(this.debug.level>2)
			this.log("Result:", success);
		
		if(success){
			if(this.cb!==undefined && typeof this.cb == "function")
				this.cb(this.__jqo);
			else
				console.log("Warning: callback cb not function", this.cb);
		}
		else{
			if(this.cbFail!==undefined && typeof this.cbFail == "function")
				this.cbFail(this.__jqo);
		}
	}
}
if("undefined" === typeof eleWaiters){ // jshint ignore:line
	var eleWaiters ={}; // jshint ignore:line
}

function waitFor(sel, cb, cbFail=null, findIn="document", delay=500, maxTries=50, alwaysOn=false, debug = false){ // 2021-01-29
	return new eleWaiter(sel, cb, cbFail, findIn, delay, maxTries, alwaysOn, true, debug);
}