Basic Functions (For userscripts)

Useful functions for myself

As of 2024-03-22. See the latest version.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/456034/1347319/Basic%20Functions%20%28For%20userscripts%29.js

  1. /* eslint-disable no-multi-spaces */
  2.  
  3. // ==UserScript==
  4. // @name Basic Functions (For userscripts)
  5. // @name:zh-CN 常用函数(用户脚本)
  6. // @name:en Basic Functions (For userscripts)
  7. // @namespace PY-DNG Userscripts
  8. // @version 0.8.4.1
  9. // @description Useful functions for myself
  10. // @description:zh-CN 自用函数
  11. // @description:en Useful functions for myself
  12. // @author PY-DNG
  13. // @license GPL-3.0-or-later
  14. // ==/UserScript==
  15.  
  16. // Note: version 0.8.2.1 is modified just the license and it's not uploaded to GF yet 23-11-26 15:03
  17. // Note: version 0.8.3.1 is added just the description of parseArgs and has not uploaded to GF yet 24-02-03 18:55
  18.  
  19. let [
  20. // Console & Debug
  21. LogLevel, DoLog, Err, Assert,
  22.  
  23. // DOM
  24. $, $All, $CrE, $AEL, $$CrE, addStyle, detectDom, destroyEvent,
  25.  
  26. // Data
  27. copyProp, copyProps, parseArgs, escJsStr, replaceText,
  28.  
  29. // Environment & Browser
  30. getUrlArgv, dl_browser, dl_GM,
  31.  
  32. // Logic & Task
  33. AsyncManager, testChecker, registerChecker, loadFuncs
  34. ] = (function() {
  35. // function DoLog() {}
  36. // Arguments: level=LogLevel.Info, logContent, logger='log'
  37. const [LogLevel, DoLog] = (function() {
  38. const LogLevel = {
  39. None: 0,
  40. Error: 1,
  41. Success: 2,
  42. Warning: 3,
  43. Info: 4,
  44. };
  45.  
  46. return [LogLevel, DoLog];
  47. function DoLog() {
  48. // Get window
  49. const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;
  50.  
  51. const LogLevelMap = {};
  52. LogLevelMap[LogLevel.None] = {
  53. prefix: '',
  54. color: 'color:#ffffff'
  55. }
  56. LogLevelMap[LogLevel.Error] = {
  57. prefix: '[Error]',
  58. color: 'color:#ff0000'
  59. }
  60. LogLevelMap[LogLevel.Success] = {
  61. prefix: '[Success]',
  62. color: 'color:#00aa00'
  63. }
  64. LogLevelMap[LogLevel.Warning] = {
  65. prefix: '[Warning]',
  66. color: 'color:#ffa500'
  67. }
  68. LogLevelMap[LogLevel.Info] = {
  69. prefix: '[Info]',
  70. color: 'color:#888888'
  71. }
  72. LogLevelMap[LogLevel.Elements] = {
  73. prefix: '[Elements]',
  74. color: 'color:#000000'
  75. }
  76.  
  77. // Current log level
  78. DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  79.  
  80. // Log counter
  81. DoLog.logCount === undefined && (DoLog.logCount = 0);
  82.  
  83. // Get args
  84. let [level, logContent, logger] = parseArgs([...arguments], [
  85. [2],
  86. [1,2],
  87. [1,2,3]
  88. ], [LogLevel.Info, 'DoLog initialized.', 'log']);
  89.  
  90. let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : '');
  91. let subst = LogLevelMap[level].color;
  92.  
  93. switch (typeof(logContent)) {
  94. case 'string':
  95. msg += '%s';
  96. break;
  97. case 'number':
  98. msg += '%d';
  99. break;
  100. default:
  101. msg += '%o';
  102. break;
  103. }
  104.  
  105. // Log when log level permits
  106. if (level <= DoLog.logLevel) {
  107. // Log to console when log level permits
  108. if (level <= DoLog.logLevel) {
  109. if (++DoLog.logCount > 512) {
  110. console.clear();
  111. DoLog.logCount = 0;
  112. }
  113. console[logger](msg, subst, logContent);
  114. }
  115. }
  116. }
  117. }) ();
  118.  
  119. // type: [Error, TypeError]
  120. function Err(msg, type=0) {
  121. throw new [Error, TypeError][type]((typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + msg);
  122. }
  123.  
  124. function Assert(val, errmsg, errtype) {
  125. val || Err(errmsg, errtype);
  126. }
  127.  
  128. // Basic functions
  129. // querySelector
  130. function $() {
  131. switch(arguments.length) {
  132. case 2:
  133. return arguments[0].querySelector(arguments[1]);
  134. break;
  135. default:
  136. return document.querySelector(arguments[0]);
  137. }
  138. }
  139. // querySelectorAll
  140. function $All() {
  141. switch(arguments.length) {
  142. case 2:
  143. return arguments[0].querySelectorAll(arguments[1]);
  144. break;
  145. default:
  146. return document.querySelectorAll(arguments[0]);
  147. }
  148. }
  149. // createElement
  150. function $CrE() {
  151. switch(arguments.length) {
  152. case 2:
  153. return arguments[0].createElement(arguments[1]);
  154. break;
  155. default:
  156. return document.createElement(arguments[0]);
  157. }
  158. }
  159. // addEventListener
  160. function $AEL(...args) {
  161. const target = args.shift();
  162. return target.addEventListener.apply(target, args);
  163. }
  164. function $$CrE() {
  165. const [tagName, props, attrs, classes, styles, listeners] = parseArgs([...arguments], [
  166. function(args, defaultValues) {
  167. const arg = args[0];
  168. return {
  169. 'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
  170. 'object': () => ['tagName', 'props', 'attrs', 'classes', 'styles', 'listeners'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
  171. }[typeof arg]();
  172. },
  173. [1,2],
  174. [1,2,3],
  175. [1,2,3,4],
  176. [1,2,3,4,5]
  177. ], ['div', {}, {}, [], {}, []]);
  178. const elm = $CrE(tagName);
  179. for (const [name, val] of Object.entries(props)) {
  180. elm[name] = val;
  181. }
  182. for (const [name, val] of Object.entries(attrs)) {
  183. elm.setAttribute(name, val);
  184. }
  185. for (const cls of Array.isArray(classes) ? classes : [classes]) {
  186. elm.classList.add(cls);
  187. }
  188. for (const [name, val] of Object.entries(styles)) {
  189. elm.style[name] = val;
  190. }
  191. for (const listener of listeners) {
  192. $AEL(...[elm, ...listener]);
  193. }
  194. return elm;
  195. }
  196.  
  197. // Append a style text to document(<head>) with a <style> element
  198. // arguments: css | css, id | parentElement, css, id
  199. // remove old one when id duplicates with another element in document
  200. function addStyle() {
  201. // Get arguments
  202. const [parentElement, css, id] = parseArgs([...arguments], [
  203. [2],
  204. [2,3],
  205. [1,2,3]
  206. ], [document.head, '', null]);
  207.  
  208. // Make <style>
  209. const style = $CrE("style");
  210. style.textContent = css;
  211. id !== null && (style.id = id);
  212. id !== null && $(`#${id}`) && $(`#${id}`).remove();
  213.  
  214. // Append to parentElement
  215. parentElement.appendChild(style);
  216. return style;
  217. }
  218.  
  219. // Get callback when specific dom/element loaded
  220. // detectDom({[root], selector, callback[, once]}) | detectDom(selector, callback) | detectDom(root, selector, callback) | detectDom(root, selector, callback, attributes) | detectDom(root, selector, callback, attributes, once)
  221. function detectDom() {
  222. let [root, selectors, callback, attributes, once] = parseArgs([...arguments], [
  223. function(args, defaultValues) {
  224. const arg = args[0];
  225. return ['root', 'selector', 'callback', 'attributes', 'once'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i]);
  226. },
  227. [2,3],
  228. [1,2,3],
  229. [1,2,3,4],
  230. [1,2,3,4,5]
  231. ], [document, [''], e => Err('detectDom: callback not found'), false, true]);
  232. !Array.isArray(selectors) && (selectors = [selectors]);
  233.  
  234. if (select(root, selectors)) {
  235. for (const elm of selectAll(root, selectors)) {
  236. callback(elm);
  237. if (once) {
  238. return null;
  239. }
  240. }
  241. }
  242.  
  243. const observer = new MutationObserver(mCallback);
  244. observer.observe(root, {
  245. childList: true,
  246. subtree: true,
  247. attributes,
  248. });
  249.  
  250. function mCallback(mutationList, observer) {
  251. const addedNodes = mutationList.reduce((an, mutation) => {
  252. switch (mutation.type) {
  253. case 'childList':
  254. an.push(...mutation.addedNodes);
  255. break;
  256. case 'attributes':
  257. an.push(mutation.target);
  258. break;
  259. }
  260. return an;
  261. }, []);
  262. const addedSelectorNodes = addedNodes.reduce((nodes, anode) => {
  263. if (anode.matches && match(anode, selectors)) {
  264. nodes.add(anode);
  265. }
  266. const childMatches = anode.querySelectorAll ? selectAll(anode, selectors) : [];
  267. for (const cm of childMatches) {
  268. nodes.add(cm);
  269. }
  270. return nodes;
  271. }, new Set());
  272. for (const node of addedSelectorNodes) {
  273. callback(node);
  274. if (once) {
  275. observer.disconnect();
  276. break;
  277. }
  278. }
  279. }
  280.  
  281. function selectAll(elm, selectors) {
  282. !Array.isArray(selectors) && (selectors = [selectors]);
  283. return selectors.map(selector => [...$All(elm, selector)]).reduce((all, arr) => {
  284. all.push(...arr);
  285. return all;
  286. }, []);
  287. }
  288.  
  289. function select(elm, selectors) {
  290. const all = selectAll(elm, selectors);
  291. return all.length ? all[0] : null;
  292. }
  293.  
  294. function match(elm, selectors) {
  295. return !!elm.matches && selectors.some(selector => elm.matches(selector));
  296. }
  297.  
  298. return observer;
  299. }
  300.  
  301. // Just stopPropagation and preventDefault
  302. function destroyEvent(e) {
  303. if (!e) {return false;};
  304. if (!e instanceof Event) {return false;};
  305. e.stopPropagation();
  306. e.preventDefault();
  307. }
  308.  
  309. // Object1[prop] ==> Object2[prop]
  310. function copyProp(obj1, obj2, prop) {obj1[prop] !== undefined && (obj2[prop] = obj1[prop]);}
  311. function copyProps(obj1, obj2, props) {(props || Object.keys(obj1)).forEach((prop) => (copyProp(obj1, obj2, prop)));}
  312.  
  313. // Argument parser with sorting and defaultValue support
  314. function parseArgs(args, rules, defaultValues=[]) {
  315. // args and rules should be array, but not just iterable (string is also iterable)
  316. if (!Array.isArray(args) || !Array.isArray(rules)) {
  317. throw new TypeError('parseArgs: args and rules should be array')
  318. }
  319.  
  320. // fill rules[0]
  321. (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);
  322.  
  323. // max arguments length
  324. const count = rules.length - 1;
  325.  
  326. // args.length must <= count
  327. if (args.length > count) {
  328. throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
  329. }
  330.  
  331. // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
  332. for (let i = 1; i <= count; i++) {
  333. const rule = rules[i];
  334. if (Array.isArray(rule)) {
  335. if (rule.length !== i) {
  336. throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
  337. }
  338. if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
  339. throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
  340. }
  341. } else if (typeof rule !== 'function') {
  342. throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
  343. }
  344. }
  345.  
  346. // Parse
  347. const rule = rules[args.length];
  348. let parsed;
  349. if (Array.isArray(rule)) {
  350. parsed = [...defaultValues];
  351. for (let i = 0; i < rule.length; i++) {
  352. parsed[rule[i]-1] = args[i];
  353. }
  354. } else {
  355. parsed = rule(args, defaultValues);
  356. }
  357. return parsed;
  358. }
  359.  
  360. // escape str into javascript written format
  361. function escJsStr(str, quote='"') {
  362. str = str.replaceAll('\\', '\\\\').replaceAll(quote, '\\' + quote).replaceAll('\t', '\\t');
  363. str = quote === '`' ? str.replaceAll(/(\$\{[^\}]*\})/g, '\\$1') : str.replaceAll('\r', '\\r').replaceAll('\n', '\\n');
  364. return quote + str + quote;
  365. }
  366.  
  367. // Replace model text with no mismatching of replacing replaced text
  368. // e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee'
  369. // replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA'
  370. // replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}'
  371. // replaceText('abcd', {}) === 'abcd'
  372. /* Note:
  373. replaceText will replace in sort of replacer's iterating sort
  374. e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT'
  375. but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was
  376. not always the case, and the order is complex. As a result, it's best not to rely on property order.
  377. So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to
  378. replace irrelevance replacer keys only.
  379. */
  380. function replaceText(text, replacer) {
  381. if (Object.entries(replacer).length === 0) {return text;}
  382. const [models, targets] = Object.entries(replacer);
  383. const len = models.length;
  384. let text_arr = [{text: text, replacable: true}];
  385. for (const [model, target] of Object.entries(replacer)) {
  386. text_arr = replace(text_arr, model, target);
  387. }
  388. return text_arr.map((text_obj) => (text_obj.text)).join('');
  389.  
  390. function replace(text_arr, model, target) {
  391. const result_arr = [];
  392. for (const text_obj of text_arr) {
  393. if (text_obj.replacable) {
  394. const splited = text_obj.text.split(model);
  395. for (const part of splited) {
  396. result_arr.push({text: part, replacable: true});
  397. result_arr.push({text: target, replacable: false});
  398. }
  399. result_arr.pop();
  400. } else {
  401. result_arr.push(text_obj);
  402. }
  403. }
  404. return result_arr;
  405. }
  406. }
  407.  
  408. // Get a url argument from location.href
  409. // also recieve a function to deal the matched string
  410. // returns defaultValue if name not found
  411. // Args: {name, url=location.href, defaultValue=null, dealFunc=((a)=>{return a;})} or (name) or (url, name) or (url, name, defaultValue) or (url, name, defaultValue, dealFunc)
  412. function getUrlArgv(details) {
  413. const [name, url, defaultValue, dealFunc] = parseArgs([...arguments], [
  414. function(args, defaultValues) {
  415. const arg = args[0];
  416. return {
  417. 'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
  418. 'object': () => ['name', 'url', 'defaultValue', 'dealFunc'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
  419. }[typeof arg]();
  420. },
  421. [2,1],
  422. [2,1,3],
  423. [2,1,3,4]
  424. ], [null, location.href, null, a => a]);
  425.  
  426. if (name === null) { return null; }
  427.  
  428. const search = new URL(url).search;
  429. const objSearch = new URLSearchParams(search);
  430. const raw = objSearch.has(name) ? objSearch.get(name) : defaultValue;
  431. const argv = dealFunc(raw);
  432.  
  433. return argv;
  434. }
  435.  
  436. // Save dataURL to file
  437. function dl_browser(dataURL, filename) {
  438. const a = document.createElement('a');
  439. a.href = dataURL;
  440. a.download = filename;
  441. a.click();
  442. }
  443.  
  444. // File download function
  445. // details looks like the detail of GM_xmlhttpRequest
  446. // onload function will be called after file saved to disk
  447. function dl_GM(details) {
  448. if (!details.url || !details.name) {return false;};
  449.  
  450. // Configure request object
  451. const requestObj = {
  452. url: details.url,
  453. responseType: 'blob',
  454. onload: function(e) {
  455. // Save file
  456. dl_browser(URL.createObjectURL(e.response), details.name);
  457.  
  458. // onload callback
  459. details.onload ? details.onload(e) : function() {};
  460. }
  461. }
  462. if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;};
  463. if (details.onprogress ) {requestObj.onprogress = details.onprogress;};
  464. if (details.onerror ) {requestObj.onerror = details.onerror;};
  465. if (details.onabort ) {requestObj.onabort = details.onabort;};
  466. if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
  467. if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;};
  468.  
  469. // Send request
  470. GM_xmlhttpRequest(requestObj);
  471. }
  472.  
  473. function AsyncManager() {
  474. const AM = this;
  475.  
  476. // Ongoing xhr count
  477. this.taskCount = 0;
  478.  
  479. // Whether generate finish events
  480. let finishEvent = false;
  481. Object.defineProperty(this, 'finishEvent', {
  482. configurable: true,
  483. enumerable: true,
  484. get: () => (finishEvent),
  485. set: (b) => {
  486. finishEvent = b;
  487. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  488. }
  489. });
  490.  
  491. // Add one task
  492. this.add = () => (++AM.taskCount);
  493.  
  494. // Finish one task
  495. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  496. }
  497.  
  498. const [testChecker, registerChecker, loadFuncs] = (function() {
  499. const checkers = {
  500. switch: value => value,
  501. url: value => location.href === value,
  502. path: value => location.pathname === value,
  503. regurl: value => !!location.href.match(value),
  504. regpath: value => !!location.pathname.match(value),
  505. starturl: value => location.href.startsWith(value),
  506. startpath: value => location.pathname.startsWith(value),
  507. func: value => value()
  508. };
  509.  
  510. // Check whether current page url matches FuncInfo.checker rule
  511. // This code is copy and modified from FunctionLoader.check
  512. function testChecker(checker) {
  513. if (!checker) {return true;}
  514. const values = Array.isArray(checker.value) ? checker.value : [checker.value];
  515. return (checker.all ? values.every : values.some)(value => {
  516. const type = checker.type;
  517. if (checkers.hasOwnProperty(type)) {
  518. try {
  519. return checkers[type](value);
  520. } catch (err) {
  521. DoLog(LogLevel.Error, 'Checker function raised an error');
  522. DoLog(LogLevel.Error, err);
  523. return false;
  524. }
  525. } else {
  526. DoLog(LogLevel.Error, 'Invalid checker type');
  527. return false;
  528. }
  529. });
  530. }
  531.  
  532. function registerChecker(name, func) {
  533. Assert(['Symbol', 'string', 'number'].includes(typeof name), 'name should be symbol, string or number');
  534. Assert(typeof func === 'function', 'func should be a function');
  535. checkers[name] = func;
  536. }
  537.  
  538. // Load all function-objs provided in funcs asynchronously, and merge return values into one return obj
  539. // funcobj: {[checker], [detectDom], func}
  540. function loadFuncs(oFuncs) {
  541. const returnObj = {};
  542.  
  543. oFuncs.forEach(oFunc => {
  544. if (!oFunc.checker || testChecker(oFunc.checker)) {
  545. if (oFunc.detectDom) {
  546. detectDom(oFunc.detectDom, e => execute(oFunc));
  547. } else {
  548. setTimeout(e => execute(oFunc), 0);
  549. }
  550. }
  551. });
  552.  
  553. return returnObj;
  554.  
  555. function execute(oFunc) {
  556. setTimeout(e => {
  557. const rval = oFunc.func(returnObj) || {};
  558. copyProps(rval, returnObj);
  559. }, 0);
  560. }
  561. }
  562.  
  563. return [testChecker, registerChecker, loadFuncs];
  564. }) ();
  565.  
  566. return [
  567. // Console & Debug
  568. LogLevel, DoLog, Err, Assert,
  569.  
  570. // DOM
  571. $, $All, $CrE, $AEL, $$CrE, addStyle, detectDom, destroyEvent,
  572.  
  573. // Data
  574. copyProp, copyProps, parseArgs, escJsStr, replaceText,
  575.  
  576. // Environment & Browser
  577. getUrlArgv, dl_browser, dl_GM,
  578.  
  579. // Logic & Task
  580. AsyncManager, testChecker, registerChecker, loadFuncs
  581. ];
  582. })();