Greasy Fork is available in English.

BP Funcs

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

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/447081/1199231/BP%20Funcs.js

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

/*
	BP Funcs, as of 2023-06-02 17:22:41 (GMT +02:00)
*/

class SelectorDef{
	constructor(selectors="", refineFunc=null, within=null){
		this.selectors = [];
		if(selectors){
			switch(typeof selectors){
				case "object":
					if(selectors instanceof Array){
						this.selectors = [...selectors];
					}
					else{
						try{
							Object.assign(this, selectors);
							return this;
						}catch(e){
							console.warn("Could not assign Object to SelectorDef with", selectors);
						}
					}
					break;
				case "string":
					this.selectors = [selectors];
					break;
				default:
					console.warn("Could not construct SelectorDef with selector of type '" + (typeof selectors) + "':", selectors);
			}
		}
		this.refineFunc = refineFunc;
		this.within = within;
		return this;
	}
	add(selectors="", refineFunc=null, within=null){
		this.selectors.push(new SelectorDef(selectors, refineFunc, within));
		return this;
	}
	find(within=null, separate=false){
		if(!within)
			within = document;
		var r;
		if(separate)
			r = { length: 0, hasEMpty: false };
		else
			r = $();
		var i = 0;
		for(let sel of this.selectors){
			let f = null;
			if(sel instanceof SelectorDef){
				f = sel.find(within);
			}
			else{
				f = $(within).find(sel);
				if(this.refineFunc)
					f = this.refineFunc(f);
			}
			if(separate){
				if(f && f.length)
					r[i] = f;
				else{
					r[i] = null;
					r.hasEMpty = true;
				}
				r.length++;
			}
			else if(f && f.length)
				r = r.add(f);
			i++;
		}
		return r;
	}
	get(separate = false){
		return this.find(this.within, separate);
	}
	waitFor(cb, stopForAny = false, requireAll = false, cbFail=null, doneClass="", interval=200, maxTries=50){
		console.log("waiting, maxTries = " + maxTries);
		var res = this.get(requireAll);
		var runAgain = true;
		if(res.length>0){
			runAgain = maxTries <= -1 && !stopForAny;
			if(requireAll){
				if(!res.hasEMpty){
					var results = res[0];
					console.log("got all?", res);
					for(let i = 1; i<res.length; i++)
						results = results.add(res[i]);
					cb(results);
				}
				else
					runAgain = true;
			}
			else
				cb(res);
		}
		else
			runAgain = true;
		if(runAgain){
			var t = this;
			if(maxTries>0){
				maxTries--;
			} else if(maxTries==0){
				if(typeof cbFail == "function")
					cbFail();
				return;
			}
			this.__timer = setTimeout(function(){
				t.waitFor(cb, stopForAny, requireAll, cbFail, doneClass, interval, maxTries);
			}, interval);
		}
	}
	stop(){
		if(this.__timer)
			clearTimeout(this.__timer);
	}
}
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);
};
function arraySortRelevance(arr, q, selectRelevantProperty = (item)=>item){
	q = q.toLowerCase().replace(/&/g, " and ");
	var words = {
		q: q.split(/[\b\s./\\'-]/)
	};
	var matchFunc = function(a, b){
		return a.localeCompare(b, undefined, {sensitivity: "accent", usage: "sort" })===0;
	};
	arr.sort(function(itemA, itemB){
		var items = {
			a: selectRelevantProperty(itemA).toLowerCase().replace(/&/g, " and "),
			b: selectRelevantProperty(itemB).toLowerCase().replace(/&/g, " and ")
		};
		
		if(matchFunc(items.a, items.b)){
			return 1;
		}

		if(matchFunc(items.a, q))
			return -1;
		if(matchFunc(items.b, q))
			return 1;

		var score = {
			a: 0,
			b: 0,
			last: {
				a: 0,
				b: 0
			}
		};
		var exactMult = 2;
		var runMult = 2;
		var missingMult = 1;
		words.a = items.a.split(/[\b\s./\\'-]/);
		words.b = items.b.split(/[\b\s./\\'-]/);
		
		for(let k of["a", "b"]){
			for(let w of words.q){
				if(w.trim()==="")
					continue;
				missingMult = 1;
				var mi = items[k].indexOf(w);
				if(mi>=0){
					var s = w.length;
					var ni = mi + s;
					var prev = items[k].substring(mi-1, mi);
					var next = items[k].substring(ni, ni+1);
					if(! /\w/.test(prev + next)){
						s *= exactMult;
					}
					score[k] += score.last[k] + s;
					score.last[k] = w.length * (runMult - 1);
				}
				else{
					score[k] -= w.length * missingMult;
					score.last[k] = 0;
				}
			}
			missingMult = 0.5;
			for(let wk of words[k]){
				if(!q.indexOf(wk)>=0){
					score[k] -= wk.length * missingMult;
				}
			}
		}
		// log(score, items, q);
		if(score.a>score.b)
			return -1;
		if(score.a<score.b)
			return 1;
		if(items.a.length<items.b.length)
			return -1;
		else
			return 1;
	});
	return arr;
}
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(style="light"){
	var r = {};
	r.obj = null;
	r.items = {};
	const styles = {
		light: {
			colors : {
				background : "#fff",
				color : "#333",
				item : "#222",
				itemHover : "#268",
				itemBack : "transparent",
				frame : "#eee",
				button:{
					background: "#bbb",
					color: "#333",
					background_hover: "#157",
					color_hover: "#fff",
				}
			},
			fontSize: "14px"
		},
		dark: {
			colors : {
				background : "#000",
				color : "#ccc",
				item : "#ddd",
				itemHover : "#6ce",
				itemBack : "transparent",
				frame : "#111",
				button:{
					background: "#444",
					color: "#ccc",
					background_hover: "#7cf",
					color_hover: "#000",
				}
			},
			fontSize: "14px"
		}
	};
	r.style = styles[style];
	if(!r.style)
		r.style = styles.light;
	r.css = `
		.bpbutton{
			padding: 5px;
			display: inline-block;
			background: ${r.style.colors.button.background};
			color: ${r.style.colors.button.color};
			cursor: pointer;
			font-weight: 600;
			line-height: 1em;
		}
		.bpbutton:hover, .bpbutton.on{
			background: ${r.style.colors.button.background_hover};
			color: ${r.style.colors.button.color_hover};
		}
		
		#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: ${r.style.colors.background};
			color: ${r.style.colors.color};
			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};
			background: ${r.style.colors.itemBack};
			font-weight: bold;
			cursor: pointer;
		}
		#bpMenu .bp.item:hover{
			color: ${r.style.colors.itemHover};
		}
		#bpMenu .bp+.bp{
			margin-left: 10px;
		}
	`;
	r.setup = function(override=false){
		var head, script;
		head = document.getElementsByTagName("head")[0];
		if(typeof $ !== "function"){
			return console.error("bpMenu: No jQuery '$'; can't continue");
			// 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"){
			return console.error("bpMenu: No bpModal; can't continue");
			// console.warn("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)
			$("body").append("<div id='bpMenu'><div class='inner'></div></div>");
		r.obj = $("body>#bpMenu");
	};
	r.injectStyles = function(override=false){
		if("undefined" != typeof GM_addStyle)
			return GM_addStyle(r.css);
		
		if(override)
			$("#bpMenuStyle").remove();
		if($("#bpMenuStyle").length<=0)
			$("head").append(`<style id="bpMenuStyle">${r.css}</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
//}
class BP_Color{
	constructor(colorOrRed, green, blue){
		this.color = {
			red: 0,
			green: 0,
			blue: 0
		};
		if(colorOrRed instanceof Color){
			this.color = colorOrRed.color;
		}
		else if(colorOrRed instanceof Array){
			this.color = {
				red: colorOrRed[0],
				green: colorOrRed[1],
				blue: colorOrRed[2]
			};
		}
		else if(typeof colorOrRed == "object"){
			this.color = {
				red: colorOrRed.red || colorOrRed.Red || colorOrRed.r || colorOrRed.R,
				green: colorOrRed.green || colorOrRed.Green || colorOrRed.g || colorOrRed.G,
				blue: colorOrRed.blue || colorOrRed.Blue || colorOrRed.b || colorOrRed.B
			};
		}
		else if(typeof colorOrRed == "string" && green === undefined){
			var hex = [];
			if(colorOrRed.startsWith("#")){
				colorOrRed = colorOrRed.replace(/^#?(\w)(\w)(\w)$/, "#$1$1$2$2$3$3");
				var c = Color.hexToRgb(colorOrRed);
				if(c){
					this.color.red = c[0];
					this.color.green = c[1];
					this.color.blue = c[2];
				}
			}
		}
		else{
			this.color.red = colorOrRed;
			this.color.green = green;
			this.color.blue = blue;
		}
		return this;
	}
	toRGB(){
		return [this.color.red, this.color.green, this.color.blue];
	}
	toHex(){
		return Color.rgbToHex(this.color.red, this.color.green, this.color.blue);
	}
	get(){
		return this.color;
	}
	static componentToHex(c) {
		var hex = c.toString(16);
		return hex.length == 1 ? "0" + hex : hex;
	}
	static rgbToHex(r, g, b) {
		return "#" + Color.componentToHex(r) + Color.componentToHex(g) + Color.componentToHex(b);
	}
	static hexToRgb(hex) {
		var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
		return result ? [ parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null;
	}
}
function colorString(str, colorOrRed, green, blue){
	var color = [0,0,0];
	if([colorOrRed, green, blue].every(v => typeof v == "number"))
		color = [colorOrRed, green, blue];
	else if(colorOrRed instanceof Array)
		color = colorOrRed;
	else{
		color = new BP_Color(colorOrRed, green, blue).toRGB();
	}
	return `\x1B[38;2;${color[0]};${color[1]};${color[2]}m${str}\x1B[0m`;
}
function convolve(func, ...partArrays){
	if(partArrays.length == 1 && !(partArrays[0] instanceof Array) && (typeof partArrays[0]) == "object")
		partArrays = Object.values(partArrays[0]);
	if(! partArrays?.length)
		return;
	var funcArgArrays = [];
	var firstParts = partArrays[0];
	
	var nextArgs = [];
	
	if(partArrays.length == 1){
		if(typeof func == "function"){
			for(let p of firstParts)
				func(p);
		}
		return firstParts;
	}
	var lastArray = false;
	if(partArrays.length > 2){
		nextArgs = convolve(false, ...partArrays.slice(1));
	}
	else{
		nextArgs = partArrays[1];
		lastArray = true;
	}
	
	for(let arg of firstParts){
		for(let otherArgs of nextArgs){
			if(lastArray)
				funcArgArrays.push([arg, otherArgs]);
			else
				funcArgArrays.push([arg, ...otherArgs]);
			if(typeof func == "function")
				func(arg, ...otherArgs);
		}
	}
	return funcArgArrays;
}
function copyToClipboard(text) {
	if (window.clipboardData && window.clipboardData.setData) {
		// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
		return window.clipboardData.setData("Text", text);

	}
	else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
		var textarea = document.createElement("textarea");
		textarea.textContent = text;
		textarea.style.position = "fixed";  // Prevent scrolling to bottom of page in Microsoft Edge.
		document.body.appendChild(textarea);
		textarea.select();
		try {
			document.execCommand("copy");  // Security exception may be thrown by some browsers.
			msg("Copied.", "success", "", 1000);
			return;
		}
		catch (ex) {
			console.warn("Copy to clipboard failed.", ex);
			return prompt("Copy to clipboard: Ctrl+C, Enter", text);
		}
		finally {
			document.body.removeChild(textarea);
		}
	}
}
if("undefined" == typeof bp_iObject){
	bp_iObject = {
		style: `<style id="bp_iO_style">
	div.bp_iObject{
		color: #fff;
	}
	div.bp_iObject div.bp_iObject{
		display: inline;
		vertical-align: text-top;
	}
	.bp_iObject div.bp_iO_inner{
		white-space: pre;
		border: 1px solid transparent;
		transition: all 0.5s;
	}
	.bp_iO_pseudoContent{
		margin: 0.4em;
	}
	.bp_iO_pseudoContent + .bp_iO_pseudoContent{
		margin-left: 0;
	}
	.bp_iO_pseudoContent:before{
		content: attr(data-value);
	}
	span.bp_iO_meta{
		opacity: 0.7;
		background: #333;
		border: 1px solid #888;
		border-radius: 0.3em;
		font-size: 66%;
	}
	span.bp_iO_indent:before{
		content: "	";
		white-space: pre;
	}
	span.bp_iO_comma{
		display: inline;
		vertical-align: bottom;
	}
	div.bp_iObject>.bp_iO_content{
		color: #bbb;
		white-space: pre;
	}
	div.bp_iObject[data-type="string"]>.bp_iO_content,
	div.bp_iObject span.quotes{
		color: #888;
	}
	span.bp_iO_key{
		font-style: italic;
	}
	.collapsible>span.bp_iO_key{
		cursor: pointer;
	}
	div.bp_iObject[data-type="string"]>.bp_iO_content>span.text{
		color: #fe8;
	}
	div.bp_iObject[data-type="number"]>.bp_iO_content{
		color: #5cf;
	}
	div.bp_iObject[data-type="RegExp"]>.bp_iO_content{
		color: #f48;
	}
	div.bp_iObject[data-type="boolean"]>.bp_iO_content{
		color: #08f;
	}
	div.bp_iObject.collapsed>.bp_iO_content{
		white-space: normal;
	}
	div.bp_iObject.collapsed .bp_iO_inner{
		display: inline-block;
		width: 1px;
		height: 1px;
		overflow: hidden;
		margin-left: -5px;
		opacity: 0.01;
		clip: rect(0 0 0 0);
	}
	div.bp_iObject.collapsible .bracket.closing{
		cursor: pointer;
		pointer-events: all;
	}
	div.bp_iObject.collapsed .bracket.closing:before{
		content: "... ";
	}
	.bp_iObject{
		position: relative;
	}
	.bp_iObject.collapsible>.bp_iO_key:before{
		content: "â–² ";
		position: absolute;
		display: inline-block;
		right: 100%;
		font-size: 66%;
		pointer-events: all;
	}
	.bp_iObject.collapsible.collapsed>.bp_iO_key:before{
		content: "â–¼ ";
	}
	.bp_iO_inner.editing{
		border: 1px inset #888;
		background: #000;
		color: #fff;
		padding: 0.5em;
		margin: 0.2em;
	}
	.bp_iO_copy{
		position: absolute;
		right: 0;
		top:0;
		opacity: 0;
		padding: 0.75em 1em;
		background: #666;
		color: #fff;
		border: 1px solid #fff;
		border-radius: 0.5em;
		cursor: pointer;
		transition: all 0.5s;
	}
	/* .bp_iO_copy::before{
		content: "\\002398  Copy";
	}
	.bp_iObject:hover>.bp_iO_copy{
		opacity: 0.3;
	}
	.bp_iObject:hover>.bp_iO_copy:hover{
		opacity: 1;
		background: #000;
	}*/ /* TODO: make palatable */
	.zeroHeight{
		line-height: 0;
		margin:0;
		padding:0;
		display: none;
	}
	.collapsed br.zeroHeight{
		display: inline;
	}
	.collapsedIndent{
		display: none;
	}
	.collapsed>.bp_iO_content>.bp_iO_inner>.collapsedIndent{
		display: inline;
	}
	</style>`,
		createInteractiveObject: function(obj, onlyOwnProperties=true, indentString="\t", objKey="", level=0, includeLabel = false){
			if("undefined" == typeof bp_iO)
				bp_iO = bp_iObject.setup();
			var indent = "";
			for(i=0; i<level; i++)
				indent += indentString;
			var q = "<span class='quotes'>&quot;</span>";
			var ell = "<span class='bp_iO_ellipsis'>&quot;</span>";
			var r = "";
			var t = typeof obj;
			var typeString = t.replace(/^\w/, (a)=> a.toUpperCase());
			var inner = "";
			var count=0;
			var isArray = obj instanceof Array;
			switch(typeof obj){
				case "object":
					var pt = ""; // + obj.toString(); // TODO: prototype class name?
					if(isArray)
						typeString = "Array";
					else if(obj instanceof RegExp){
						typeString = t = "RegExp";
						inner = obj.toString();
						break;
					}
					typeString += " " + pt;
					if(!obj || !obj.keys){
						inner = obj.toString();
						break;
					}
					var keys = Object.keys(obj);
					if(onlyOwnProperties){
						var okeys = keys;
						keys = [];
						for(let k of okeys){
							if(obj.hasOwnProperty(k))
								keys.push(k);
						}
					}
					count = keys.length;
					for(let i=0; i<keys.length; i++){
						let k = keys[i];
						inner += (isArray?"":`${indent + indentString}`) + createInteractiveObject(obj[k], onlyOwnProperties, indentString, k, level+1, isArray?false:true) + (i<keys.length-1?"<span class='bp_iO_comma'>, </span>" + (isArray?"":"<br />"):"");
					}
					if(!isArray && count>0)
						inner = "<div class='bp_iO_inner'><br class='zeroHeight' />" + inner + "<br /><span class='collapsedIndent'>" + indent + "</span></div>";
					if(isArray)
						inner = "[" + inner + "]";
					else{
						inner = "{" + inner + (inner?indent:"") + "<span class='bracket closing'>}</span>";
					}
				break;
				case "string":
					inner += q + "<span class='text bp_iO_inner'>" + htmlEntities(obj) + "</span>" + q;
					break;
				default:
					inner += "<span class='bp_iO_inner'>" + htmlEntities(obj.toString()) + "</span>";
			}
			r = (objKey && includeLabel?`<span class='bp_iO_key'>${q + objKey + q}</span><span class='bp_iO_type bp_iO_meta bp_iO_pseudoContent' data-value='${typeString}'></span>` + (t=="object"?`<span class='bp_iO_count bp_iO_meta bp_iO_pseudoContent' data-value='(${count})'></span>`:"") + ":&nbsp;" :"") + "<span class='bp_iO_content'>" + inner + "</span>";
			
			r = `<div class="bp_iObject${t=="object" && !(obj instanceof Array) && count>0?" collapsible":""}${level<=0?" root editable":""}" data-type="${t}" data-level="${level}" data-key="${objKey}"><div class="bp_iO_copy"></div>${r}</div>`;
			
			if(level <= 0){
				r = $(r)[0];
				r.bp_iO = {
					ref: obj,
					onlyOwnProperties,
					indentString,
					element: r,
					redraw: function(){
						var e = createInteractiveObject(this.ref, this.onlyOwnProperties, this.indentString);
						this.element.innerHTML = e.innerHTML;
						$(e).remove();
						// console.log(this.element);
					}
				};
			}
			return r;
		},
		setup: function(){
			if("undefined" == typeof bp_iO){
				bp_iO = this;
				$("head").append(this.style);
				$("body").on("click", ".bp_iO_copy", function(e){
					e.stopImmediatePropagation();
					// console.log(e, e.target);
					var r = $(e.target).parent().text();
					copyToClipboard(r);
				});
				$("body").on("click", ".collapsible>.bp_iO_key, .collapsible>.bp_iO_content>.bracket.closing", function(e){
					e.stopImmediatePropagation();
					// console.log(e, e.target);
					var p = $(e.target).parent();
					if(p.hasClass("bp_iO_content"))
						p = p.parent();
					p.toggleClass("collapsed");
				});
				$("body").on("dblclick", ".bp_iObject.root.editable .bp_iO_inner", function(e){
					// if(!$(e.target).is(e.currentTarget))
					// 	return;
					e.stopImmediatePropagation();
					// console.log(e, e.target);
					var o = $(e.currentTarget);
					o.addClass("editing");
					// console.log(e.target, o);
					o.attr("contenteditable", "true");
					o[0].beforeEdit = o.text().trim();
					o[0].focus();
					return false;
				});
				$(document).on("keydown", ".bp_iO_inner.editing", function(e){
					// console.log(e);
					var o = $(e.target);
					if(e.key=="Enter" && !e.shiftKey){
						// console.log("enter!");
						var val_inner = o.text().trim();
						var val_container = o.closest(".bp_iObject").first();
						var val_outer = o.parent().text().trim();
						// console.log("new value?", val_outer, val_inner);
						var path = [];
						var obj = o.parents(".bp_iObject.root");
						if(!obj || obj.length<=0 || !obj[0].bp_iO){
							console.error("Dang, no root found for", o);
						}
						else{
							try{
								obj = obj[0].bp_iO;
								var ref = obj.ref;
								var v = eval(`(function(){return ${val_outer};})();`);
								if(val_container.is(o.parents(".bp_iObject.root").first())){
									// console.log("yup, modifying root");
									for(let k in ref) // TODO: accommodate different types
										delete ref[k];
									for(let k in v)
										ref[k] = v[k];
									obj.redraw();
								}
								else{
									var ancestry = o.parentsUntil(".bp_iObject.root", ".bp_iObject").get();
									ancestry = ancestry.reverse();
									
									for(let i = 0; i<ancestry.length; i++){
										var p = ancestry[i];
										// var key = p.attr("data-key");
										var k = p.dataset.key;
										// console.log(k, p);
										if(i>=ancestry.length-1){
											// console.log(`(function(){return ${val_outer};})();`);
											ref[k] = v;
											var h = createInteractiveObject(ref[k], obj.onlyOwnProperties, obj.indentString, k, i+1, i+1>0);
											val_container.html(h);
											break;
										}
										ref = ref[k];
										path.push(k);
									}
									// console.log(obj, path)
								}
							}catch(e){
								console.error(e);
							}
						}
					}
					else if(e.key=="Escape"){
						// console.log("Esc!");
						o.text(o[0].beforeEdit);
					}
					else
						return;
					
					e.preventDefault();
					o.removeClass("editing");
					o.attr("contenteditable", "false");
				});
				createInteractiveObject = this.createInteractiveObject;
				return this;
			}
		}
	};
}
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 = pad(date.getFullYear(), 4);
	var months = pad(date.getMonth() + 1);
	var days =  pad(date.getDate());
	var hours = pad(date.getHours());
	var minutes = pad(date.getMinutes());
	var seconds = pad(date.getSeconds());
	return format.replace("YYYY", year)
		.replace("MM", months)
		.replace("DD", days)
		.replace("HH", hours)
		.replace("mm", minutes)
		.replace("SS", seconds);
}
function pad(num, digits=2){
	var r = String(num);
	if(r.padStart)
		return r.padStart(digits, "0");
	return ("0000000000" + r).slice(-digits);
}
function escapeHtml(str){
	return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
String.prototype.escapeHtml = function(){
	return escapeHtml(this);
};
filterTables = {count: 0, hasInit: false};
	
filterTables.init = function(filter="", override=false){
	if($("head #bp_filtertable").length<=0)
		$("head").append(`
			<style id="bp_filtertable">
				.filtertable.table,
				.filtertable .table {
					display: table;
				}
				.filtertable .tr {
					display: table-row;
				}
				.filtertable .td,
				.filtertable .th {
					display: table-cell;
				}
				.filtertable th,
				.filtertable .th {
					white-space: nowrap;
					word-break: keep-all;
					padding: 3px;
					text-align: left;
					font-weight: bold;
				}
				.filtertable .filterhide, .simplehide{
					display: none;
				}
				
				.filtertable th.filtering{
					background-color: rgba(200,20,0,0.3);
				}
				
				.filtertable .columnFilter{
					display: inline-block;
					vertical-align: middle;
					width: calc(100% - 20px);
				}
				.filtertable .columnFilter::placeholder{
					font-weight: 300;
				}
				
				.filtertable .sorter{
					display: inline-block;
					vertical-align: middle;
					margin-left: 3px
				}
				
				.filtertable .sort{
					padding: 1px 4px;
					line-height: 9px;
					font-size: 10px;
					background-color: rgba(140,140,140,0.5);
					color: #ccc;
					cursor: pointer;
					opacity: 0.5;
				}
				
				.filtertable .sort+.sort{
					margin-top:2px;
				}
				
				.filtertable .sort.desc:after{
					content: "â–¼";
				}
				
				.filtertable .sort.asc:after{
					content: "â–²";
				}
				
				.filtertable .sort:hover{
					opacity: 1;
				}
				
				.filtertable .sort.active{
					background-color: #160;
					color: #fff;
				}
				
				.filtertable .clearfilter{
					display: inline-block;
					vertical-align: middle;
					margin-left: -15px;
					padding: 0 3px 4px;
					background: rgba(120,10,0,1);
					color: #fff;
					opacity: 0.5;
					cursor: pointer;
				}
				.filtertable .clearfilter:hover{
					opacity: 1;
				}
		</style>
		`);
	
	$(".filtertable" + filter + ":not(.hasSetup)").each(function(){
		makeTableSortable(this, true);
	});
	
	if(filterTables.hasInit && !override)
		return;
	
	$("body").on("keyup", ".filtertable .columnFilter", function(e){
		// console.log(this, e);
		var t = $(this).closest("table, .table");
		if(!t || t.length<1)
			return console.error("Table not found filtering");
		var headers = t[0].filterHeaders;
		if(!headers)
			headers = t;
		var cont = t.children("tbody, .tbody");
		if(cont && cont.length>0)
			t = cont.first();
		var n = $(this).attr("data-colnum");
		var o = this;
		clearTimeout(filterTables.tuFilter);
		filterTables.tuFilter = setTimeout(function(){
			// console.log("filtering", n, t);
			var f = o.value;
			var fheader = $(headers).find("th:nth-child(" + n + "), .th:nth-child(" + n + ")");
			if(f == ""){
				$(t).find("tr, .tr").each(function(){
					if(filterTables.setColumnFilter(this, n, false))
						filterTables.showhideFilter(this);
				});
				$(fheader).removeClass("filtering");
				return;
			}
			$(fheader).addClass("filtering");
	
			$(t).find("tr, .tr").each(function(){
				if(this == fheader || $(this).hasClass("sortHeaderRow"))
					return;
				var c = $(this).find("td:nth-child(" + n + "), .td:nth-child(" + n + ")");
				//			console.log("got cells?", c);
				if(c.length > 0){
					var s = c.text();
					//				console.log("compare", f, "against", s, "in", n+" -> nth child");
					var fs = f.substr(0, 1);
					var fc = true;
					if(fs == "<" && f.length > 1){
						//					console.log("is <");
						fc = s * 1 >= f.substr(1) * 1;
					} else if(fs == ">" && f.length > 1){
						//					console.log("is >");
						fc = s * 1 <= f.substr(1) * 1;
					} else {
						fc = !s.toLowerCase().includes(f.toLowerCase());
					}
					if(filterTables.setColumnFilter(this, n, fc)){
						filterTables.showhideFilter(this);
					}
				}
			});
		}, 100);
	});

	$("body").on("click", ".filtertable .sort", function(){
		var isactive = !$(this).hasClass("active");
		filterTables.intable(this, ".sort").removeClass("active");
		$(this).toggleClass("active", isactive);
		var sortcol = 0;
		var asc = true;
		if(isactive){
			sortcol = $(this).parent().attr("data-colnum");
			asc = $(this).hasClass("asc");
		}
		filterTables.sortTable($(this).closest("table, .table").first(), sortcol, asc);
	});
	
	$("body").on("click", ".clearfilter", function(){
		var fi = $(this).parent().find(".columnFilter");
		//	console.log("clear", fi);
		//	if(fi.length>0){
		$(fi).val("");
		$(fi).keyup();
		//	}
	});

	$("body").on("click", "table.minimized tr, .table.minimized .tr", function(){
		$(this).parent().find("tr.active, .tr.active").removeClass("active");
		$(this).addClass("active");
	});
	
	setTimeout(filterTables.resizeFilters, 1000);
	setInterval(filterTables.resizeFilters, 5000);
	filterTables.hasInit = true;
};

filterTables.tuFilter = null;

filterTables.setColumnFilter = function(e, c, onoff){
	var cols = [];
	var ec = $(e).attr("filteredCols");
	if(undefined !== ec && ec != "")
		cols = ec.split(",");
	var i = cols.indexOf(c);
	//	console.log("Set filter for column " + c + " to " + onoff.toString() + " on: ", e);
	if(i < 0 == onoff){
		if(onoff){
			cols.push(c);
			//			console.log("column filter added");
		} else {
			//			console.log("remove column filter:");
			var a = cols.splice(i, 1);
			//			console.log("a:", a);
			//			console.log("cols:", cols);
		}
		$(e).attr("filteredCols", cols.join(","));
		//		console.log("Needed change, is now: " + $(e).attr("filteredCols"));
		return true;
	}
	//	console.log("no change");
	return false;
};

filterTables.showhideFilter = function(e){
	var ec = $(e).attr("filteredCols");
	if(undefined !== ec && ec != "")
		$(e).addClass("filterhide");
	else
		$(e).removeClass("filterhide");
};

filterTables.sortTable = function(table, col, asc){
	var tbody = $(table).children("tbody, .tbody");
	if(tbody.length>0)
		table = tbody;
	var tosort = $(table).children("tr.sortable, .tr.sortable");
	var ifasc = asc ? 1 : -1;
	
	// console.log("sorting:", table, tosort);
	tosort.sort(function(a, b){
		var ca, cb;
		if(col === 0){
			ca = $(a).attr("nosort");
			cb = $(b).attr("nosort");
		} else {
			ca = $(a).find("td:nth-child(" + col + "), .td:nth-child(" + col + ")").text();
			cb = $(b).find("td:nth-child(" + col + "), .td:nth-child(" + col + ")").text();
		}
		
		ca = ca.trim().toLowerCase();
		cb = cb.trim().toLowerCase();
		
		if((ca*1).toString() == ca.toString() && (cb*1).toString() == cb.toString()){
			// console.log("is numeric");
			return (ca*1 - cb*1) * ifasc;
		}
		else{
			var rex = /\s*(\d+)(.*)/;
			var ma = ca.match(rex);
			var mb = cb.match(rex);
			if(ma && mb){
				// console.log("oh, numbers!", ma, mb);
				var diff = ma[1] - mb[1];
				if(diff!=0)
					return diff * ifasc;
			}
		}
		
		if(ca > cb)
			return 1 * ifasc;
		if(ca < cb)
			return -1 * ifasc;
		return 0;
	});
	// $(table).children("tr.sortable, .tr.sortable").remove();
	$(table).append(tosort);
};

filterTables.intable = function(el, s){
	return $(el).closest("table, .table").first().find(s);
};

filterTables.resizeFilters = function(){
	$(".shrink .columnFilter").each(function(){
		var o = this;
		filterTables.shrinkifbigger(o);
	});
};

filterTables.shrinkifbigger = function(o){
	var p = $(o).parent().parent();
	var w = p.width() - 22;
	if(w<40)
		w = 40;
	if(w > $(p).width()-20)
		return;
	$(o).css("width", w+"px");
	// setTimeout(function(){
		if(w > $(p).width()-20){
			filterTables.shrinkifbigger(o);
			// console.log("shrink: " + p.text());
		}
		else{
			// console.log("is max: " + p.text);
		}
	// }, 1);
};

function makeTableSortable(element, hasInit=false){
	var table = $(element);
	var headers;
	if(table.is("tr, .tr")){
		headers = table;
		table = headers.closest("table, .table");
	}
	else{
		let cont = table.children("thead, .thead, tbody, .tbody").first();
		if(!cont || cont.length<=0)
			cont = table;
		headers = cont.find(">tr>th, >.tr>th, >tr>.th, >.tr>.th");
		if(headers && headers.length>0)
			headers = headers.parent();
		else
			headers = cont.find(">tr, >.tr").first();
	}
	headers.addClass("sortHeaderRow").children("td, .td").addClass("th"); // in case they're just td, .td
	table.addClass("filtertable sortable");
	table[0].filterHeaders = headers;
	
	filterTables.count++;
	var c = 0;
	$(headers).children("th, .th").each(function(){
		var h = $(this).html();
		c++;
		var w = $(this).width() - 16;
		if(w<40)
			w = 40;
		$(this).html("<div class='filterable'>" + h + "</div><div class='filters'><input class='columnFilter' style='widdth: " + w + "px;' name='columnFilter-" + c + "' value='' data-colnum='" + c + "' placeholder='Filter \"" + h + "\"' /><div class='clearfilter'>x</div><div class='sorter' data-colnum='" + c + "'><div class='sort asc'></div><div class='sort desc'></div></div></div>").addClass("sortHeader");
	});
	c = 0;
	var cont = table.children("tbody, .tbody");
	if(!cont || cont.length<1)
		cont = table;
	$(cont).children("tr:not(.sortHeaderRow), .tr:not(.sortHeaderRow)").each(function(){
		// if(c > 0){
			$(this).addClass("sortable");
			$(this).attr("nosort", c);
		// }
		c++;
	});
	table.addClass("hasSetup");
	
	if(!filterTables.hasInit && !hasInit)
		filterTables.init();
}
function filteredDeepCopy(obj, func, maxDepth=10, circ=[]){
	var out = {};
	if(obj instanceof Array)
		out = [];
	for(let k in obj){
		if(!Object.hasOwn(obj, k))
			continue;
		let v = obj[k];
		if(typeof func == "function"){
			let o = func(k, v);
			if(!o)
				continue;
			if(o instanceof Array){
				k = o[0];
				v = o[1];
			}
		}
		if(typeof v == "object"){
			if(maxDepth<=0 || circ.includes(v))
				continue;
			maxDepth--;
			out[k] = filteredDeepCopy(v, func, maxDepth, [...circ, v]);
			continue;
		}
		out[k] = v;
	}
	return out;
}
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*[,-]?\s*(?:episode |E|x)0*(\d+(?:-\d+)?)(?: ?[:, -]* (.+))?/mi
	]
};
function testEpisodeTitleRex(rex){
	var values = {
		name: ["Some series name", "Test? <^> \"Toast\""],
		season: ["0", "03", "102"],
		episode: ["0", "03", "102", "13-14"],
		title: ["and a title", "\"and a title\"", " - and a title"],
		joiners: [",", ":", " :", "-", " -", ""],
		altjoiners: [",", "-", " -"],
		sseason: ["season ", "s"],
		sepisode: ["episode ", "e"]
	};
	if(!rex)
		rex = bpMediaTitleRegex.series[0];
	var testStrings = [];
	convolve((name, season, episode, title, joiner, altjoiner, sseason, sepisode)=>{
			testStrings.push(`${name}${joiner} ${sseason}${season}${altjoiner} ${sepisode}${episode}${joiner} ${title}`);
			testStrings.push(`${name}${joiner} ${sseason}${season}${altjoiner}${sepisode}${episode}${joiner} ${title}`);
			
			testStrings.push(`NoTitle${name}${joiner} ${sseason}${season}${altjoiner} ${sepisode}${episode}`);
			testStrings.push(`NoTitle${name}${joiner} ${sseason}${season}${altjoiner}${sepisode}${episode}`);
			
			testStrings.push(`${name} ${season}x${episode}${joiner} ${title}`);
			testStrings.push(`NoTitle${name} ${season}x${episode}`);
		}, 
		values
	);
		
	// log(testStrings);
	var colorOk = "#66ff55";
	var colorWarn = "#cccc55";
	var colorBad = "#ff6655";
	var testOk = [];
	var testFail = [];
	
	for(let s of testStrings){
		s = s.replace(/\s+/g, " ");
		var m = s.match(rex);
		// log(r);
		if(!m || !m.length){
			log(colorString(s, colorWarn), colorString("Failed RegEx test", colorBad));
			testFail.push([s, "Failed to match RegEx"]);
		}
		else{
			// log(colorString(s, colorOk));
			var name = m[1];
			var season = m[2];
			var episode = m[3];
			var title = m[4];
			if(title){
				title = title.trim().replace(/^[,: -]+/g, "");
				title = title.trim().replace(/^"(.+)"$/, "$1");
			}
			var isNoTitle = false;
			if(name.startsWith("NoTitle")){
				name = name.replace(/^NoTitle/, "");
				isNoTitle = true;
			}
			
			if(!values.name.includes(name)){
				testFail.push([s, "Failed NAME"]);
				continue;
			}
			if(!["0", "3", "102"].includes(season)){
				testFail.push([s, "Failed SEASON"]);
				continue;
			}
			if(!["0", "3", "102", "13-14"].includes(episode)){
				testFail.push([s, "Failed EPISODE"]);
				continue;
			}
			if(!["and a title"].includes(title) && !isNoTitle){
				testFail.push([s, "Failed TITLE"]);
				continue;
			}
			testOk.push(s);
		}
	}
	for(let f of testFail)
		log(f[0], colorString(f[1], colorBad));
	log(colorString("Test OK: " + testOk.length, colorOk), colorString("Test Failed: " + testFail.length, testFail.length?colorBad:colorOk));
}

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, "-")
		.replace(/["]/g, "'")
		.replace(/\?\?/g, "⁇")
		.replace(/\?/g, "︖")
		.replace(/\s*[/:]\s*/g, " - ")
		.replace(/\s+-\s*(?:-+\s+)+/g, " - ")
		.replace(/\s+/g, " ");
	return str.trim();
}

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

function varToPretty(str, casing="title", isSecond=false){
	str = str
		.replace(/(?:([^A-Z])([A-Z]))|(?:([a-zA-Z])([^a-zA-Z]))/g, "$1$3 $2$4")
		.replace(/_/g, " ")
		.replace(/\s\s+/g, " ")
		.replace(/(max|min)/gi, "$1imum");
	if(casing == "title")
		str = str.replace(/(^|\s+)([a-z])/g, (_, a, b) => a + b.toUpperCase());
	if(isSecond)
		return str;
	return varToPretty(str, casing, true);
}

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 getProperties(obj, filter=false, skipNative = true, _stringify=false){
	var r = {};
	var i = 0;
	for(let k in obj){
		let v = obj[k];
		let include = true;
		switch(typeof filter){
			case "function":
				include = filter(v);
				break;
			case "object":
				if(filter instanceof RegExp)
					include = filter.test(k);
				break;
			case "string":
				let types = filter.split(/[, ]/);
				include = false;
				let inverted = false;
				for(let t of types){
					t = t.trim();
					if(t == "")
						continue;
					if(t.substr(0,1)=="!"){
						inverted = true;
						t = t.substr(1);
					}
					if((t == "function" && v && v.call)){
						if(!inverted){
							v = v.toString();
							include = !skipNative || v.indexOf("[native code]")<0;
						}
						else
							include = false;
						break;
					}
					else if((t == typeof v) ^ inverted){
						include = true;
						break;
					}
				}
				break;
		}
		if(include){
			if(_stringify && (typeof v != "string")){
				let maybeRemoveQuotes = false;
				try{
					v = JSON.stringify(v, function(k, v){
						if(typeof v == "function" || (v && v.call)){
							v = v.toString();
							maybeRemoveQuotes = true;
							return v;
						}
						return v;
					}, "\t");
				}catch(e){
					maybeRemoveQuotes = false;
					v = v.toString();
				}
				if(maybeRemoveQuotes && typeof v == "string" && v.startsWith("\"") && v.endsWith("\""))
					v = v.substring(1, v.length-1);
			}
			r[k] = v;
		}
	}
	return r;
}
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, nl2br=false){
	str = str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
		return '&#' + i.charCodeAt(0) + ';';
	});
	if(nl2br)
		str = str.replace(/\r?\n/g, "<br />");
	return str;
}
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 isPrimitive(obj){
	if(typeof obj == "undefined" || obj === null)
		return true;
	if(typeof obj == "object" || typeof obj == "function")
		return false;
	return true;
}
function hasJQuery(){
	if(typeof $ == "function" && typeof $.prototype == "object" && typeof $.fn != "undefined")
		return $;
	if(typeof jQuery == "function" && typeof jQuery.prototype == "object" && typeof jQuery.fn != "undefined")
		return jQuery;
	return false;
}

function isJQuery(obj){
	if(!obj || typeof obj != "object")
		return false;
	var jq = hasJQuery();
	if(!jq)
		return false;
	return obj instanceof jq;
}

function jqExtend(jQ=false){
	if(!jQ)
		jQ = hasJQuery();
	if(jQ){
		var fn = jQ.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 (level > this.level)
			return;
		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(style="light"){
	var r = {};
	r.messages = [];
	r.count = 0;
	const styles = {
		light: {
			colors: {
				msg: {
					border: "#666",
					background: "#eee",
					color: "#333"
				},
				error: {
					border: "#a10",
					background: "#ffcabf",
					color: "#610"
				},
				warn: {
					border: "#cc4",
					background: "#fec",
					color: "#880"
				},
				success: {
					border: "#190",
					background: "#bf9",
					color: "#070"
				},
				modal: {
					border: "#fff",
					background: "#eee",
					color: "#333"
				},
				modalClose: {
					border: "#fff",
					background: "#000",
					color: "#fff"
				}
			}
		},
		dark: {
			colors: {
				msg: {
					border: "#aaa",
					background: "#222",
					color: "#ddd"
				},
				error: {
					border: "#722",
					background: "#300",
					color: "#c66"
				},
				warn: {
					border: "#cc4",
					background: "#330",
					color: "#dd6"
				},
				success: {
					border: "#190",
					background: "#040",
					color: "#6d6"
				},
				modal: {
					border: "#333",
					background: "#000",
					color: "#ccc"
				},
				modalClose: {
					border: "#fff",
					background: "#000",
					color: "#fff"
				}
			}
		},
	};
	r.style = styles[style];
	if(!r.style)
		r.style = styles.light;
	r.css = `#messageoverlays{
			position:fixed;
			top:20vh;
			z-index:1000;
			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 ${r.style.colors.msg.border};
			background-color: ${r.style.colors.msg.background};
			color: ${r.style.colors.msg.color};
		}
		#messageoverlays .msg.error{
			border: 1px solid ${r.style.colors.error.border};
			background-color: ${r.style.colors.error.background};
			color: ${r.style.colors.error.color};
		}
		
		#messageoverlays .msg.warn{
			border: 1px solid ${r.style.colors.warn.border};
			background-color: ${r.style.colors.warn.background};
			color: ${r.style.colors.warn.color};
		}

		#messageoverlays .msg.success{
			border: 1px solid ${r.style.colors.success.border};
			background-color: ${r.style.colors.success.background};
			color: ${r.style.colors.success.color};
		}

		.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 ${r.style.colors.modal.border};
			display:block;
			background-color: ${r.style.colors.modal.background};
			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: ${r.style.colors.modalClose.background};
			color: ${r.style.colors.modalClose.color};
			opacity:0.7;
			position:absolute;
			right:-12px;
			top:-12px;
			-webkit-border-radius: 20px;
			-moz-border-radius: 20px;
			border-radius: 20px;
			border:4px solid ${r.style.colors.modalClose.border};
			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: ${r.style.colors.modal.color};
			overflow: unset;
			display:block;
			text-align: left;
		}
		.bpModback .table{
			display:table;
		}
		
		.bpModback .tr{
			display:table-row;
		}
		
		.bpModback .td{
			display: table-cell;
		}
		#watch #player{
			height: 200px;
		}`;
	
	r.setup = function(){
		// TODO: retrofill
		if(typeof $ !== "function"){
			return console.error("bpMenu: No jQuery '$'; can't continue");
			// 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;
		}
//		console.log("Actual setup with jQuery");
		if($("head #bpModalStyle").length<=0){
			$("head").append(`<style id="bpModalStyle">
					${r.css}
				</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;
			}
		}
	}
	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) {
	if (typeof str != "string") {
		console.warn("called regEsc with non-string:", str);
		return str;
	}
	return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};

String.prototype.replaceAllInsensitive = function(str1, str2)
{
	return this.replace(new RegExp(regEsc(str1), "gi"),(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();
}
class elementSelector{
	constructor(element=false, attributes=true){
		this.tagName = 
		this.id = 
		this.className = 
		this.sibling = 
		"";
		this._attribs = {};
		if(element){
			if(isJQuery(element)){
				if(element.length<=0)
					return;
				element = element[0];
			}
			if(element instanceof HTMLElement){
				var t = this;
				this.element = element;
				this.tagName = element.tagName.toLowerCase();
				if(element.id)
					this.id = "#" + element.id;
				if(element.className){
					this.className = "." + element.className.trim().replace(/\s+/g, ".");
				}
				if(attributes)
					for(let attr of element.attributes){
						if(["id", "class"].includes(attr.name))
							continue;
						if(typeof attributes == "string")
							attributes = [attributes];
						if(attributes instanceof Array && !attributes.includes(attr.name))
							continue;
						if(attr.value)
							this._attribs[attr.name] = attr.value;
					}
				
				var p = element.parentElement;
				if(p && p.childElementCount>1){
					var sibs = p.querySelectorAll(":scope > " + this.tagName);
					if(sibs && sibs.length>1){
						for(let i = 0; i<sibs.length; i++){
							let sib = sibs[i];
							if(sib == element){
								// console.log("found in siblings:", el);
								if(i==0)
									t.sibling = ":first-of-type";
								else if(i==sibs.length-1)
									t.sibling = ":last-of-type";
								else
									t.sibling = ":nth-of-type(" + (i+1) + ")";
								return false;
							}
							// console.log("not same:", sib, el);
						}
					}
				}
			}
		}
	}
	attribs(filter=false){
		var r = "";
		if(typeof filter == "string")
			filter = [filter];
		for(let k in this._attribs){
			if(filter instanceof Array && !filter.includes(k))
				continue;
			let v = this._attribs[k];
			r += `[${k}="${v}"]`;
		}
		return r;
	}
	get attributes(){
		return this.attribs();
	}
	clone(){
		var r = new elementSelector();
		Object.assign(r, this);
		r._attribs = {};
		for(let k in this._attribs)
			r._attribs[k] = this._attribs[k];
		return r;
	}
	toString(includeAttribs = true, includeSibling = true){
		return this.tagName + this.id + this.className + (includeAttribs?this.attribs(includeAttribs):"") + (includeSibling?this.sibling:"");
	}
	countMatches(descendants = null, includeAttribs = true, includeSibling = true){
		if(descendants)
			return descendants.add(this, true).toString(includeAttribs, includeSibling);
		var s = this.toString(includeAttribs, includeSibling);
		if(s)
			return document.querySelectorAll(s).length;
		return 0;
	}
}
class elementSelectorChain{
	constructor(elements = false, reverse = false, attributes=true){
		this.selectors = [];
		if(elements){
			if(isJQuery(elements))
				elements = elements.get();
			
			if(elements instanceof Array){
				if(reverse)
					elements = elements.reverse();
				for(let e of elements){
					if(!e){
						this.add(null);
						continue;
					}
					if(e instanceof elementSelector)
						this.add(e);
					else
						this.add(new elementSelector(e, attributes));
				}
			}
			else if(elements instanceof HTMLElement)
				this.add(new elementSelector(elements, attributes));
			else console.warn("elementSelectorChain: Could not construct with", elements, ", starting empty");
		}
	}
	add(selector, toStart=false){
		if(toStart){
			this.selectors = [selector, ...this.selectors];
			return this;
		}
		this.selectors.push(selector);
		return this;
	}
	each(func, reverse=false){
		var l = this.selectors.length-1;
		for(let i = 0; i<=l; i++){
			let j = reverse?l-i:i;
			let e = this.selectors[j];
			let r  = func(e, j);
			if(r===false)
				break;
			if(r === null || r instanceof elementSelector)
				this.selectors[j] = r;
		}
	}
	length(includeEmpty=false){
		if(includeEmpty)
			return this.selectors.length;
		var c = 0;
		for(let s of this.selectors)
			if(s) c++;
		return c;
	}
	get(ix){
		return this.selectors[ix];
	}
	getElements(){
		var r = [];
		var s = this.toString();
		if(s)
			r = document.querySelectorAll(s);
		return r;
	}
	clone(){
		var r = new elementSelectorChain();
		this.each(function(s){
			if(!s)
				r.add(null);
			else
				r.add(s.clone());
		});
		return r;
	}
	toString(includeAttribs = true, includeSibling = true){
		var r = "";
		var isDirect = true;
		var isFirst = true;
		for(let e of this.selectors){
			if(!e){
				isDirect = false;
				continue;
			}
			r += (isFirst?"":(isDirect?" > ":" ")) + e.toString(includeAttribs, includeSibling);
			isDirect = true;
			isFirst = false;
		}
		return r;
	}
	countMatches(includeAttribs = true, includeSibling = true){
		var s = this.toString(includeAttribs, includeSibling);
		if(s)
			return document.querySelectorAll(s).length;
		return 0;
	}
}

/**
 * @name getSelector
 * @description Get a matching CSS selector for the given HTML Element
 * @param {(HTMLElement|jQuery)} element - The object (instance of HTMLElement or jQuery/$) for which to compose the selector
 * @param {boolean} [minimal=true] - Try to trim selectors that don&apos;t narrow down the search on this page? (default: true)
 * @param {(boolean|string|string[]|"auto")} [attributes="auto"] - Should attributes be included? (default: "auto")
 * - string/string[]: Filter by attribute name[list] 
 * - \"auto\": include if it narrows down the search
 * @param {boolean} [preferTopDown=true] - Try to trim unnecessary selectors closest to the element. Otherwise, trim unnecessary selectors closest to the document root. Only significant when minimal==true (default: true)
 * @returns {string} CSS Selector
 */
function getSelector(element, minimal = true, attributes = "auto", preferTopDown = true){
	return traverseAncestryForSelectors(element, minimal, attributes, preferTopDown).toString();
}
function getSelectors(elements, minimal = true, attributes = "auto", preferTopDown = true){
	var r = [];
	for(let element of elements)
		r.push(traverseAncestryForSelectors(element, minimal, attributes, preferTopDown).toString());
	return r;
}

function getSelectorChoices(element, attributes = "auto"){
	var r = [];
	r.push(traverseAncestryForSelectors(element, true, attributes, true).toString());
	r.push(traverseAncestryForSelectors(element, true, attributes, false).toString());
	r.push(traverseAncestryForSelectors(element, false, attributes).toString());
	return r;
}

function traverseAncestryForSelectors(element, minimal = true, attributes = "auto", preferTopDown = true){
	if(!element)
		throw new Error("traverseAncestryForSelectors: Not a valid element");
	if(isJQuery(element)){
		if(element.length<=0)
			throw new Error("traverseAncestryForSelectors: No element in jQuery object");
		element = element[0];
	}
	var ancestry = [];
	if(typeof $ == "function" && typeof $.prototype == "object" && typeof $(element) == "object" && typeof $(element).parents == "function"){
		ancestry = $(element).add($(element).parents());
	}
	else{
		var p = element;
		while(p && p != document){
			ancestry.push(p);
			p = p.parentElement;
		}
		ancestry = ancestry.reverse();
	}
	
	var fullChain = new elementSelectorChain(ancestry, false, attributes);
	var tmpChain = fullChain.clone();
	var chain = new elementSelectorChain();
	var cs = fullChain.toString(attributes);
	if(!cs)
		throw new Error("Selector empty");
	
	var bestCount = document.querySelectorAll(cs).length;
	
	if(bestCount<=0){
		console.warn("Selector chain too restrictive/broken?", cs, fullChain);
	}
	// console.log("Complete chain:", cs, fullChain, bestCount==1?"UNIQUE":bestCount);
	
	function applyIfViable(tmpSelector, ix){
		if(fullChain.selectors[ix] === null || fullChain.selectors[ix] == tmpSelector)
			return fullChain.selectors[ix];
		if(tmpSelector && (tmpSelector.tagName + tmpSelector.id + tmpSelector.className) == "")
			return fullChain.selectors[ix];
		tmpChain.selectors[ix] = tmpSelector;
		let els = tmpChain.getElements();
		// console.log(els.length, bestCount, els.is(element), els, element);
		if(els.length>bestCount || els[0] != element){
			tmpChain.selectors[ix] = fullChain.selectors[ix].clone();
		}
		else{
			if(tmpSelector)
				fullChain.selectors[ix] = tmpSelector.clone();
			else
				fullChain.selectors[ix] = null;
			// console.log("set to ", tmpSelector, fullChain.selectors[ix]);
		}
		return fullChain.selectors[ix];
	}
		
	// console.log("going DOWN");
	fullChain.each(function(sel, ix){
		// console.log(ix, sel.toString(), tmpChain.selectors[ix]);
		if(!sel || !sel.sibling)
			return;
		var c = sel.clone();
		c.sibling = "";
		// console.log("before sib check", ix, c, tmpChain.selectors[ix]);
		applyIfViable(c, ix);
		// console.log("after sib check", tmpChain.selectors[ix]);
	});
	
	// console.log("going UP", fullChain.toString());
	
	if(!minimal)
		return fullChain;
	
	fullChain.each(function(sel, ix){
		// console.log(ix, sel.toString());
		var c = sel;
		if(minimal && ix<=fullChain.length()-1)
			c = applyIfViable(null, ix);
		if(c){
			c = c.clone();
			if(attributes=="auto")
				for(let k in c._attribs){
					// console.log("before attrib remove", k, c._attribs);
					delete c._attribs[k];
					// console.log("before attrib check", k, c._attribs);
					c = applyIfViable(c, ix).clone();
					// console.log("after attrib check", k, c._attribs);
				}
			
			for(let k of ["tagName", "className"]){
				// console.log("before attrib remove", ix, k, c);
				c[k] = "";
				// console.log("before attrib check", k, c);
				c = applyIfViable(c, ix).clone();
				// console.log("after attrib check", k, c);
			}
		}
		else{
			// console.log("skipped", sel.toString());
		}
		chain.add(c, preferTopDown);
		//console.log("currently at", s);
		if(minimal && c && document.querySelector(chain.toString()) == element)
			return false;
	}, preferTopDown);
		
	return chain;
}
// TODO: option to filter attribs by specific values also, not just keys
/* 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 toVarName(str){
	return str
		.replace(/\s+(\w)/g, function(_, a){
			return a.toUpperCase();
		})
		.replace(/[^a-zA-Z0-9]/g, "_")
		.replace(/^([0-9])/, "_$1");
}
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=null, delay=500, maxTries=50, alwaysOn=false, autoStart=true, debug = false, logFunc = null){
		this.sel = "";
		this.cb = null;
		this.cbFail = null;
		this.findIn = null;
		this.delay = 500;
		this.maxTries = 50;
		this.alwaysOn = false;
		this.autoStart = true;
		this.debug = false;
		this.logFunc = null;

		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)
				this.findIn = findIn;
			this.delay = delay;
			this.maxTries = maxTries;
			this.alwaysOn = alwaysOn;
			this.autoStart = autoStart;
			this.debug = debug;
			this.logFunc = logFunc;
		}
		
		if(!(this.sel instanceof Array)){  // 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 = "";
			
				if(!this.debug.level)
					this.debug.level = 1;
			}
			else{
				this.debug = {
					prefix: "",
					level: 1
				};
			}
		}
		
		if(!this.logFunc){
			var prefix = "";
			if(this.debug)
				prefix = this.debug.prefix;
			
			if(typeof BPLogger != "undefined"){
				var logger = new BPLogger(prefix ? prefix + " EleWaiter" : "EleWaiter");
				this.logFunc = logger.log.bind(logger);
				// this.debug.prefix = false;
			}
			else{
				this.logFunc = function(...args){
					console.log("EleWaiter:", ...args);
				};
			}
		}
		this.log(this, 3);
		if(this.autoStart)
			this.__wait();
	}
	
	log(...args){
		if(!this.debug)
			return;
		if(typeof args == "object" && args instanceof Array && args.length>=2 && typeof args[args.length-1] == "number"){
			var level = args[args.length-1];
			if(level>this.debug.level){
				return;
			}
			args.pop();
		}
		this.logFunc(...args);
	}

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

	__wait(){
		if(!this.findIn || this.findIn == "document"){
			if(!!document)
				this.findIn = document;
			else
				this.findIn = $(":root");
		}
		
		this.__running = true;
		if(this.maxTries!=-1)
			this.__tries++;
		var triesLeft = this.alwaysOn?1:(this.maxTries - this.__tries);
		this.log("tries left:", triesLeft, 3);
		this.__jqo = $();
		for(let sel of this.sel){
			if(typeof sel == "function"){ // 2022-07-11: predicate style
				this.log("sel is func:", this.sel, 3);
				jqo = $(this.findIn);
				var res = sel(jqo);
				if(!res){
					if(!this.alwaysOn)
						this.log("Not true:", sel.toString(), "for", this.findIn, 3);
					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(res);
					this.log("Found something, is now:", this.__jqo, 3);
				}
				continue;
			}
			var jqo = $(this.findIn).find(sel);

			if(jqo.length<=0){
				if(!this.alwaysOn)
					this.log("Not found: " + sel, "in", this.findIn, 3);
				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, 3);
			}
		}
		this.__result(this.__jqo);

		if(this.alwaysOn){
			this.log("Always on, repeat", 3);
			this.__timer = setTimeout(function(){this.__wait();}.bind(this), this.delay);
		}
	}
	__result(success=false){
		if(!this.alwaysOn){
			this.__running = false;
			this.log("Result:", success, 2);
		}else if(this.debug.level>2)
			this.log("Result:", success, 1);
		
		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);
}