Greasy Fork is available in English.

Basic Functions

自用函数

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.org/scripts/449412/1122880/Basic%20Functions.js

/* eslint-disable no-multi-spaces */

// ==UserScript==
// @name               Basic Functions
// @name:zh-CN         常用函数
// @name:en            Basic Functions
// @namespace          Wenku8++
// @version            0.8
// @description        自用函数 For wenku8++
// @description:zh-CN  自用函数 For wenku8++
// @description:en     Useful functions for myself
// @author             PY-DNG
// @license            GPL-license
// @grant              GM_info
// @grant              GM_addStyle
// @grant              GM_addElement
// @grant              GM_deleteValue
// @grant              GM_listValues
// @grant              GM_addValueChangeListener
// @grant              GM_removeValueChangeListener
// @grant              GM_setValue
// @grant              GM_getValue
// @grant              GM_log
// @grant              GM_getResourceText
// @grant              GM_getResourceURL
// @grant              GM_registerMenuCommand
// @grant              GM_unregisterMenuCommand
// @grant              GM_openInTab
// @grant              GM_xmlhttpRequest
// @grant              GM_download
// @grant              GM_getTab
// @grant              GM_saveTab
// @grant              GM_getTabs
// @grant              GM_notification
// @grant              GM_setClipboard
// @grant              GM_info
// @grant              unsafeWindow
// ==/UserScript==

const LogLevel = {
	None: 0,
	Error: 1,
	Success: 2,
	Warning: 3,
	Info: 4,
}

// Arguments: level=LogLevel.Info, logContent, trace=false
// Needs one call "DoLog();" to get it initialized before using it!
function DoLog() {
	// Get window
	const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;

	const LogLevelMap = {};
	LogLevelMap[LogLevel.None] = {
		prefix: '',
		color: 'color:#ffffff'
	}
	LogLevelMap[LogLevel.Error] = {
		prefix: '[Error]',
		color: 'color:#ff0000'
	}
	LogLevelMap[LogLevel.Success] = {
		prefix: '[Success]',
		color: 'color:#00aa00'
	}
	LogLevelMap[LogLevel.Warning] = {
		prefix: '[Warning]',
		color: 'color:#ffa500'
	}
	LogLevelMap[LogLevel.Info] = {
		prefix: '[Info]',
		color: 'color:#888888'
	}
	LogLevelMap[LogLevel.Elements] = {
		prefix: '[Elements]',
		color: 'color:#000000'
	}

	// Current log level
	DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error

	// Log counter
	DoLog.logCount === undefined && (DoLog.logCount = 0);

	// Get args
	let [level, logContent, trace] = parseArgs([...arguments], [
		[2],
		[1,2],
		[1,2,3]
	], [LogLevel.Info, 'DoLog initialized.', false]);

	// Log when log level permits
	if (level <= DoLog.logLevel) {
		let msg = '%c' + LogLevelMap[level].prefix + (typeof MODULE_DATA === 'object' ? '[' + MODULE_DATA.name + ']' : '') + (LogLevelMap[level].prefix ? ' ' : '');
		let subst = LogLevelMap[level].color;

		switch (typeof(logContent)) {
			case 'string':
				msg += '%s';
				break;
			case 'number':
				msg += '%d';
				break;
			case 'object':
				msg += '%o';
				break;
		}

		if (++DoLog.logCount > 512) {
			console.clear();
			DoLog.logCount = 0;
		}
		console[trace ? 'trace' : 'log'](msg, subst, logContent);
	}
}
DoLog();

// Basic functions
// querySelector
function $() {
	switch (arguments.length) {
		case 2:
			return arguments[0].querySelector(arguments[1]);
			break;
		default:
			return document.querySelector(arguments[0]);
	}
}
// querySelectorAll
function $All() {
	switch (arguments.length) {
		case 2:
			return arguments[0].querySelectorAll(arguments[1]);
			break;
		default:
			return document.querySelectorAll(arguments[0]);
	}
}
// createElement
function $CrE() {
	switch (arguments.length) {
		case 2:
			return arguments[0].createElement(arguments[1]);
			break;
		default:
			return document.createElement(arguments[0]);
	}
}
// addEventListener
function $AEL(...args) {
	const target = args.shift();
	return target.addEventListener.apply(target, args);
}
// Object1[prop] ==> Object2[prop]
function copyProp(obj1, obj2, prop) {
	obj1.hasOwnProperty(prop) && (obj2[prop] = obj1[prop]);
}
function copyProps(obj1, obj2, props) {
	(props || Object.keys(obj1)).forEach((prop) => (copyProp(obj1, obj2, prop)));
}

function clearChildNodes(elm) {
	for (const el of elm.childNodes) {
		elm.removeChild(el);
	}
}

// Just stopPropagation and preventDefault
function destroyEvent(e) {
	if (!e) {
		return false;
	};
	if (!e instanceof Event) {
		return false;
	};
	e.stopPropagation();
	e.preventDefault();
}

// GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
// Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
// (If the request is invalid, such as url === '', will return false and will NOT make this request)
// If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
// Requires: function delItem(){...} & function uniqueIDMaker(){...}
function GMXHRHook(maxXHR = 5) {
	const GM_XHR = GM_xmlhttpRequest;
	const getID = uniqueIDMaker();
	let todoList = [],
		ongoingList = [];
	GM_xmlhttpRequest = safeGMxhr;

	function safeGMxhr() {
		// Get an id for this request, arrange a request object for it.
		const id = getID();
		const request = {
			id: id,
			args: arguments,
			aborter: null
		};

		// Deal onload function first
		dealEndingEvents(request);

		/* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
		// Stop invalid requests
		if (!validCheck(request)) {
			return false;
		}
		*/

		// Judge if we could start the request now or later?
		todoList.push(request);
		checkXHR();
		return makeAbortFunc(id);

		// Decrease activeXHRCount while GM_XHR onload;
		function dealEndingEvents(request) {
			const e = request.args[0];

			// onload event
			const oriOnload = e.onload;
			e.onload = function() {
				reqFinish(request.id);
				checkXHR();
				oriOnload ? oriOnload.apply(null, arguments) : function() {};
			}

			// onerror event
			const oriOnerror = e.onerror;
			e.onerror = function() {
				reqFinish(request.id);
				checkXHR();
				oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
			}

			// ontimeout event
			const oriOntimeout = e.ontimeout;
			e.ontimeout = function() {
				reqFinish(request.id);
				checkXHR();
				oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
			}

			// onabort event
			const oriOnabort = e.onabort;
			e.onabort = function() {
				reqFinish(request.id);
				checkXHR();
				oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
			}
		}

		// Check if the request is invalid
		function validCheck(request) {
			const e = request.args[0];

			if (!e.url) {
				return false;
			}

			return true;
		}

		// Call a XHR from todoList and push the request object to ongoingList if called
		function checkXHR() {
			if (ongoingList.length >= maxXHR) {
				return false;
			};
			if (todoList.length === 0) {
				return false;
			};
			const req = todoList.shift();
			const reqArgs = req.args;
			const aborter = GM_XHR.apply(null, reqArgs);
			req.aborter = aborter;
			ongoingList.push(req);
			return req;
		}

		// Make a function that aborts a certain request
		function makeAbortFunc(id) {
			return function() {
				let i;

				// Check if the request haven't been called
				for (i = 0; i < todoList.length; i++) {
					const req = todoList[i];
					if (req.id === id) {
						// found this request: haven't been called
						delItem(todoList, i);
						return true;
					}
				}

				// Check if the request is running now
				for (i = 0; i < ongoingList.length; i++) {
					const req = todoList[i];
					if (req.id === id) {
						// found this request: running now
						req.aborter();
						reqFinish(id);
						checkXHR();
					}
				}

				// Oh no, this request is already finished...
				return false;
			}
		}

		// Remove a certain request from ongoingList
		function reqFinish(id) {
			let i;
			for (i = 0; i < ongoingList.length; i++) {
				const req = ongoingList[i];
				if (req.id === id) {
					ongoingList = delItem(ongoingList, i);
					return true;
				}
			}
			return false;
		}
	}
}

// Get a url argument from lacation.href
// also recieve a function to deal the matched string
// returns defaultValue if name not found
// Args: {url=location.href, name, dealFunc=((a)=>{return a;}), defaultValue=null} or 'name'
function getUrlArgv(details) {
	typeof(details) === 'string' && (details = {
		name: details
	});
	typeof(details) === 'undefined' && (details = {});
	if (!details.name) {
		return null;
	};

	const url = details.url ? details.url : location.href;
	const name = details.name ? details.name : '';
	const dealFunc = details.dealFunc ? details.dealFunc : ((a) => {
		return a;
	});
	const defaultValue = details.defaultValue ? details.defaultValue : null;
	const matcher = new RegExp('[\\?&]' + name + '=([^&#]+)');
	const result = url.match(matcher);
	const argv = result ? dealFunc(result[1]) : defaultValue;

	return argv;
}

// Append a style text to document(<head>) with a <style> element
function addStyle(css, id) {
	const style = document.createElement("style");
	id && (style.id = id);
	style.textContent = css;
	for (const elm of $All(document, '#' + id)) {
		elm.parentElement && elm.parentElement.removeChild(elm);
	}
	document.head.appendChild(style);
}

// Save dataURL to file
function saveFile(dataURL, filename) {
	const a = document.createElement('a');
	a.href = dataURL;
	a.download = filename;
	a.click();
}

// File download function
// details looks like the detail of GM_xmlhttpRequest
// onload function will be called after file saved to disk
function downloadFile(details) {
	if (!details.url || !details.name) {
		return false;
	};

	// Configure request object
	const requestObj = {
		url: details.url,
		responseType: 'blob',
		onload: function(e) {
			// Save file
			saveFile(URL.createObjectURL(e.response), details.name);

			// onload callback
			details.onload ? details.onload(e) : function() {};
		}
	}
	if (details.onloadstart) {
		requestObj.onloadstart = details.onloadstart;
	};
	if (details.onprogress) {
		requestObj.onprogress = details.onprogress;
	};
	if (details.onerror) {
		requestObj.onerror = details.onerror;
	};
	if (details.onabort) {
		requestObj.onabort = details.onabort;
	};
	if (details.onreadystatechange) {
		requestObj.onreadystatechange = details.onreadystatechange;
	};
	if (details.ontimeout) {
		requestObj.ontimeout = details.ontimeout;
	};

	// Send request
	GM_xmlhttpRequest(requestObj);
}

// get '/' splited API array from a url
function getAPI(url = location.href) {
	return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
}

// get host part from a url(includes '^https://', '/$')
function getHost(url = location.href) {
	const match = location.href.match(/https?:\/\/[^\/]+\//);
	return match ? match[0] : match;
}

function AsyncManager() {
	const AM = this;

	// Ongoing xhr count
	this.taskCount = 0;

	// Whether generate finish events
	let finishEvent = false;
	Object.defineProperty(this, 'finishEvent', {
		configurable: true,
		enumerable: true,
		get: () => (finishEvent),
		set: (b) => {
			finishEvent = b;
			b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
		}
	});

	// Add one task
	this.add = () => (++AM.taskCount);

	// Finish one task
	this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
}

// Polyfill String.prototype.replaceAll
// replaceValue does NOT support regexp match groups($1, $2, etc.)
function polyfill_replaceAll() {
	String.prototype.replaceAll = String.prototype.replaceAll ? String.prototype.replaceAll : PF_replaceAll;

	function PF_replaceAll(searchValue, replaceValue) {
		const str = String(this);

		if (searchValue instanceof RegExp) {
			const global = RegExp(searchValue, 'g');
			if (/\$/.test(replaceValue)) {
				console.error('Error: Polyfilled String.protopype.replaceAll does support regexp groups');
			};
			return str.replace(global, replaceValue);
		} else {
			return str.split(searchValue).join(replaceValue);
		}
	}
}

function randint(min, max) {
	return Math.floor(Math.random() * (max - min + 1)) + min;
}

// Replace model text with no mismatching of replacing replaced text
// e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee'
//      replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA'
//      replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}'
//      replaceText('abcd', {}) === 'abcd'
/* Note:
	    replaceText will replace in sort of replacer's iterating sort
	    e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT'
	    but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was
	    not always the case, and the order is complex. As a result, it's best not to rely on property order.
	    So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to
	    replace irrelevance replacer keys only.
	*/
function replaceText(text, replacer) {
	if (Object.entries(replacer).length === 0) {return text;}
	const [models, targets] = Object.entries(replacer);
	const len = models.length;
	let text_arr = [{text: text, replacable: true}];
	for (const [model, target] of Object.entries(replacer)) {
		text_arr = replace(text_arr, model, target);
	}
	return text_arr.map((text_obj) => (text_obj.text)).join('');

	function replace(text_arr, model, target) {
		const result_arr = [];
		for (const text_obj of text_arr) {
			if (text_obj.replacable) {
				const splited = text_obj.text.split(model);
				for (const part of splited) {
					result_arr.push({text: part, replacable: true});
					result_arr.push({text: target, replacable: false});
				}
				result_arr.pop();
			} else {
				result_arr.push(text_obj);
			}
		}
		return result_arr;
	}
}

// escape str into javascript written format
function escJsStr(str, quote='"') {
	str = str.replaceAll('\\', '\\\\').replaceAll(quote, '\\' + quote);
	quote === '`' && (str = str.replaceAll(/(\$\{[^\}]*\})/g, '\\$1'));
	return quote + str + quote;
}

function parseArgs(args, rules, defaultValues=[]) {
	// args and rules should be array, but not just iterable (string is also iterable)
	if (!Array.isArray(args) || !Array.isArray(rules)) {
		throw new TypeError('parseArgs: args and rules should be array')
	}

	// fill rules[0]
	(!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);

	// max arguments length
	const count = rules.length - 1;

	// args.length must <= count
	if (args.length > count) {
		throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
	}

	// rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
	for (let i = 1; i <= count; i++) {
		const rule = rules[i];
		if (Array.isArray(rule)) {
			if (rule.length !== i) {
				throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
			}
			if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
				throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
			}
		} else if (typeof rule !== 'function') {
			throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
		}
	}

	// Parse
	const rule = rules[args.length];
	let parsed;
	if (Array.isArray(rule)) {
		parsed = [...defaultValues];
		for (let i = 0; i < rule.length; i++) {
			parsed[rule[i]-1] = args[i];
		}
	} else {
		parsed = rule(args, defaultValues);
	}
	return parsed;
}

// Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
function delItem(arr, delIndex) {
	arr = arr.slice(0, delIndex).concat(arr.slice(delIndex + 1));
	return arr;
}

// type: [Error, TypeError]
function Err(msg, type=0) {
	throw new [Error, TypeError][type]((typeof MODULE_DATA === 'object' ? '[' + MODULE_DATA.name + ']' : '') + msg);
}