Basic Functions (For userscripts)

Useful functions for myself

Verze ze dne 24. 03. 2024. Zobrazit nejnovější verzi.

Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greatest.deepsurf.us/scripts/456034/1348286/Basic%20Functions%20%28For%20userscripts%29.js

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