Basic Functions (For userscripts)

Useful functions for myself

2023-09-15 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

بۇ قوليازمىنى بىۋاسىتە قاچىلاشقا بولمايدۇ. بۇ باشقا قوليازمىلارنىڭ ئىشلىتىشى ئۈچۈن تەمىنلەنگەن ئامبار بولۇپ، ئىشلىتىش ئۈچۈن مېتا كۆرسەتمىسىگە قىستۇرىدىغان كود: // @require https://update.greatest.deepsurf.us/scripts/456034/1250467/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.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-license
  14. // ==/UserScript==
  15.  
  16. let [
  17. // Console & Debug
  18. LogLevel, DoLog, Err,
  19.  
  20. // DOM
  21. $, $All, $CrE, $AEL, $$CrE, addStyle, detectDom, destroyEvent,
  22.  
  23. // Data
  24. copyProp, copyProps, parseArgs, escJsStr, replaceText,
  25.  
  26. // Environment & Browser
  27. getUrlArgv, dl_browser, dl_GM,
  28.  
  29. // Logic & Task
  30. AsyncManager,
  31. ] = (function() {
  32. // function DoLog() {}
  33. // Arguments: level=LogLevel.Info, logContent, logger='log'
  34. const [LogLevel, DoLog] = (function() {
  35. const LogLevel = {
  36. None: 0,
  37. Error: 1,
  38. Success: 2,
  39. Warning: 3,
  40. Info: 4,
  41. };
  42.  
  43. return [LogLevel, DoLog];
  44. function DoLog() {
  45. // Get window
  46. const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;
  47.  
  48. const LogLevelMap = {};
  49. LogLevelMap[LogLevel.None] = {
  50. prefix: '',
  51. color: 'color:#ffffff'
  52. }
  53. LogLevelMap[LogLevel.Error] = {
  54. prefix: '[Error]',
  55. color: 'color:#ff0000'
  56. }
  57. LogLevelMap[LogLevel.Success] = {
  58. prefix: '[Success]',
  59. color: 'color:#00aa00'
  60. }
  61. LogLevelMap[LogLevel.Warning] = {
  62. prefix: '[Warning]',
  63. color: 'color:#ffa500'
  64. }
  65. LogLevelMap[LogLevel.Info] = {
  66. prefix: '[Info]',
  67. color: 'color:#888888'
  68. }
  69. LogLevelMap[LogLevel.Elements] = {
  70. prefix: '[Elements]',
  71. color: 'color:#000000'
  72. }
  73.  
  74. // Current log level
  75. DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  76.  
  77. // Log counter
  78. DoLog.logCount === undefined && (DoLog.logCount = 0);
  79.  
  80. // Get args
  81. let [level, logContent, logger] = parseArgs([...arguments], [
  82. [2],
  83. [1,2],
  84. [1,2,3]
  85. ], [LogLevel.Info, 'DoLog initialized.', 'log']);
  86.  
  87. let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : '');
  88. let subst = LogLevelMap[level].color;
  89.  
  90. switch (typeof(logContent)) {
  91. case 'string':
  92. msg += '%s';
  93. break;
  94. case 'number':
  95. msg += '%d';
  96. break;
  97. default:
  98. msg += '%o';
  99. break;
  100. }
  101.  
  102. // Log when log level permits
  103. if (level <= DoLog.logLevel) {
  104. // Log to console when log level permits
  105. if (level <= DoLog.logLevel) {
  106. if (++DoLog.logCount > 512) {
  107. console.clear();
  108. DoLog.logCount = 0;
  109. }
  110. console[logger](msg, subst, logContent);
  111. }
  112. }
  113. }
  114. }) ();
  115.  
  116. // type: [Error, TypeError]
  117. function Err(msg, type=0) {
  118. throw new [Error, TypeError][type]((typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + msg);
  119. }
  120.  
  121. // Basic functions
  122. // querySelector
  123. function $() {
  124. switch(arguments.length) {
  125. case 2:
  126. return arguments[0].querySelector(arguments[1]);
  127. break;
  128. default:
  129. return document.querySelector(arguments[0]);
  130. }
  131. }
  132. // querySelectorAll
  133. function $All() {
  134. switch(arguments.length) {
  135. case 2:
  136. return arguments[0].querySelectorAll(arguments[1]);
  137. break;
  138. default:
  139. return document.querySelectorAll(arguments[0]);
  140. }
  141. }
  142. // createElement
  143. function $CrE() {
  144. switch(arguments.length) {
  145. case 2:
  146. return arguments[0].createElement(arguments[1]);
  147. break;
  148. default:
  149. return document.createElement(arguments[0]);
  150. }
  151. }
  152. // addEventListener
  153. function $AEL(...args) {
  154. const target = args.shift();
  155. return target.addEventListener.apply(target, args);
  156. }
  157. function $$CrE() {
  158. const [tagName, props, attrs, classes, styles, listeners] = parseArgs([...arguments], [
  159. function(args, defaultValues) {
  160. const arg = args[0];
  161. return {
  162. 'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
  163. 'object': () => ['tagName', 'props', 'attrs', 'classes', 'styles', 'listeners'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
  164. }[typeof arg]();
  165. },
  166. [1,2],
  167. [1,2,3],
  168. [1,2,3,4],
  169. [1,2,3,4,5]
  170. ], ['div', {}, {}, [], {}, []]);
  171. const elm = $CrE(tagName);
  172. for (const [name, val] of Object.entries(props)) {
  173. elm[name] = val;
  174. }
  175. for (const [name, val] of Object.entries(attrs)) {
  176. elm.setAttribute(name, val);
  177. }
  178. for (const cls of Array.isArray(classes) ? classes : [classes]) {
  179. elm.classList.add(cls);
  180. }
  181. for (const [name, val] of Object.entries(styles)) {
  182. elm.style[name] = val;
  183. }
  184. for (const listener of listeners) {
  185. $AEL(...[elm, ...listener]);
  186. }
  187. return elm;
  188. }
  189.  
  190. // Append a style text to document(<head>) with a <style> element
  191. // arguments: css | css, id | parentElement, css, id
  192. // remove old one when id duplicates with another element in document
  193. function addStyle() {
  194. // Get arguments
  195. const [parentElement, css, id] = parseArgs([...arguments], [
  196. [2],
  197. [2,3],
  198. [1,2,3]
  199. ], [document.head, '', null]);
  200.  
  201. // Make <style>
  202. const style = $CrE("style");
  203. style.textContent = css;
  204. id !== null && (style.id = id);
  205. id !== null && $(`#${id}`) && $(`#${id}`).remove();
  206.  
  207. // Append to parentElement
  208. parentElement.appendChild(style);
  209. return style;
  210. }
  211.  
  212. // Get callback when specific dom/element loaded
  213. // detectDom({[root], selector, callback[, once]}) | detectDom(selector, callback) | detectDom(root, selector, callback) | detectDom(root, selector, callback, once)
  214. function detectDom() {
  215. let [root, selectors, callback, once] = parseArgs([...arguments], [
  216. function(args, defaultValues) {
  217. const arg = args[0];
  218. return ['root', 'selector', 'callback', 'once'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i]);
  219. },
  220. [2,3],
  221. [1,2,3],
  222. [1,2,3,4]
  223. ], [document, [''], e => Err('detectDom: callback not found'), true]);
  224. !Array.isArray(selectors) && (selectors = [selectors]);
  225.  
  226. if (select(root, selectors)) {
  227. for (const elm of selectAll(root, selectors)) {
  228. callback(elm);
  229. if (once) {
  230. return null;
  231. }
  232. }
  233. }
  234.  
  235. const observer = new MutationObserver(mCallback);
  236. observer.observe(root, {
  237. childList: true,
  238. subtree: true
  239. });
  240.  
  241. function mCallback(mutationList, observer) {
  242. const addedNodes = mutationList.reduce((an, mutation) => ((an.push.apply(an, mutation.addedNodes), an)), []);
  243. const addedSelectorNodes = addedNodes.reduce((nodes, anode) => {
  244. if (anode.matches && match(anode, selectors)) {
  245. nodes.add(anode);
  246. }
  247. const childMatches = anode.querySelectorAll ? selectAll(anode, selectors) : [];
  248. for (const cm of childMatches) {
  249. nodes.add(cm);
  250. }
  251. return nodes;
  252. }, new Set());
  253. for (const node of addedSelectorNodes) {
  254. callback(node);
  255. if (once) {
  256. observer.disconnect();
  257. break;
  258. }
  259. }
  260. }
  261.  
  262. function selectAll(elm, selectors) {
  263. !Array.isArray(selectors) && (selectors = [selectors]);
  264. return selectors.map(selector => [...$All(elm, selector)]).reduce((all, arr) => {
  265. all.push(...arr);
  266. return all;
  267. }, []);
  268. }
  269.  
  270. function select(elm, selectors) {
  271. const all = selectAll(elm, selectors);
  272. return all.length ? all[0] : null;
  273. }
  274.  
  275. function match(elm, selectors) {
  276. return !!elm.matches && selectors.some(selector => elm.matches(selector));
  277. }
  278.  
  279. return observer;
  280. }
  281.  
  282. // Just stopPropagation and preventDefault
  283. function destroyEvent(e) {
  284. if (!e) {return false;};
  285. if (!e instanceof Event) {return false;};
  286. e.stopPropagation();
  287. e.preventDefault();
  288. }
  289.  
  290. // Object1[prop] ==> Object2[prop]
  291. function copyProp(obj1, obj2, prop) {obj1[prop] !== undefined && (obj2[prop] = obj1[prop]);}
  292. function copyProps(obj1, obj2, props) {(props || Object.keys(obj1)).forEach((prop) => (copyProp(obj1, obj2, prop)));}
  293.  
  294. function parseArgs(args, rules, defaultValues=[]) {
  295. // args and rules should be array, but not just iterable (string is also iterable)
  296. if (!Array.isArray(args) || !Array.isArray(rules)) {
  297. throw new TypeError('parseArgs: args and rules should be array')
  298. }
  299.  
  300. // fill rules[0]
  301. (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);
  302.  
  303. // max arguments length
  304. const count = rules.length - 1;
  305.  
  306. // args.length must <= count
  307. if (args.length > count) {
  308. throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
  309. }
  310.  
  311. // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
  312. for (let i = 1; i <= count; i++) {
  313. const rule = rules[i];
  314. if (Array.isArray(rule)) {
  315. if (rule.length !== i) {
  316. throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
  317. }
  318. if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
  319. throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
  320. }
  321. } else if (typeof rule !== 'function') {
  322. throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
  323. }
  324. }
  325.  
  326. // Parse
  327. const rule = rules[args.length];
  328. let parsed;
  329. if (Array.isArray(rule)) {
  330. parsed = [...defaultValues];
  331. for (let i = 0; i < rule.length; i++) {
  332. parsed[rule[i]-1] = args[i];
  333. }
  334. } else {
  335. parsed = rule(args, defaultValues);
  336. }
  337. return parsed;
  338. }
  339.  
  340. // escape str into javascript written format
  341. function escJsStr(str, quote='"') {
  342. str = str.replaceAll('\\', '\\\\').replaceAll(quote, '\\' + quote).replaceAll('\t', '\\t');
  343. str = quote === '`' ? str.replaceAll(/(\$\{[^\}]*\})/g, '\\$1') : str.replaceAll('\r', '\\r').replaceAll('\n', '\\n');
  344. return quote + str + quote;
  345. }
  346.  
  347. // Replace model text with no mismatching of replacing replaced text
  348. // e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee'
  349. // replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA'
  350. // replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}'
  351. // replaceText('abcd', {}) === 'abcd'
  352. /* Note:
  353. replaceText will replace in sort of replacer's iterating sort
  354. e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT'
  355. but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was
  356. not always the case, and the order is complex. As a result, it's best not to rely on property order.
  357. So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to
  358. replace irrelevance replacer keys only.
  359. */
  360. function replaceText(text, replacer) {
  361. if (Object.entries(replacer).length === 0) {return text;}
  362. const [models, targets] = Object.entries(replacer);
  363. const len = models.length;
  364. let text_arr = [{text: text, replacable: true}];
  365. for (const [model, target] of Object.entries(replacer)) {
  366. text_arr = replace(text_arr, model, target);
  367. }
  368. return text_arr.map((text_obj) => (text_obj.text)).join('');
  369.  
  370. function replace(text_arr, model, target) {
  371. const result_arr = [];
  372. for (const text_obj of text_arr) {
  373. if (text_obj.replacable) {
  374. const splited = text_obj.text.split(model);
  375. for (const part of splited) {
  376. result_arr.push({text: part, replacable: true});
  377. result_arr.push({text: target, replacable: false});
  378. }
  379. result_arr.pop();
  380. } else {
  381. result_arr.push(text_obj);
  382. }
  383. }
  384. return result_arr;
  385. }
  386. }
  387.  
  388. // Get a url argument from lacation.href
  389. // also recieve a function to deal the matched string
  390. // returns defaultValue if name not found
  391. // Args: {url=location.href, name, dealFunc=((a)=>{return a;}), defaultValue=null} or 'name'
  392. function getUrlArgv(details) {
  393. typeof(details) === 'string' && (details = {name: details});
  394. typeof(details) === 'undefined' && (details = {});
  395. if (!details.name) {return null;};
  396.  
  397. const url = details.url ? details.url : location.href;
  398. const name = details.name ? details.name : '';
  399. const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;});
  400. const defaultValue = details.defaultValue ? details.defaultValue : null;
  401. const matcher = new RegExp('[\\?&]' + name + '=([^&#]+)');
  402. const result = url.match(matcher);
  403. const argv = result ? dealFunc(result[1]) : defaultValue;
  404.  
  405. return argv;
  406. }
  407.  
  408. // Save dataURL to file
  409. function dl_browser(dataURL, filename) {
  410. const a = document.createElement('a');
  411. a.href = dataURL;
  412. a.download = filename;
  413. a.click();
  414. }
  415.  
  416. // File download function
  417. // details looks like the detail of GM_xmlhttpRequest
  418. // onload function will be called after file saved to disk
  419. function dl_GM(details) {
  420. if (!details.url || !details.name) {return false;};
  421.  
  422. // Configure request object
  423. const requestObj = {
  424. url: details.url,
  425. responseType: 'blob',
  426. onload: function(e) {
  427. // Save file
  428. dl_browser(URL.createObjectURL(e.response), details.name);
  429.  
  430. // onload callback
  431. details.onload ? details.onload(e) : function() {};
  432. }
  433. }
  434. if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;};
  435. if (details.onprogress ) {requestObj.onprogress = details.onprogress;};
  436. if (details.onerror ) {requestObj.onerror = details.onerror;};
  437. if (details.onabort ) {requestObj.onabort = details.onabort;};
  438. if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
  439. if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;};
  440.  
  441. // Send request
  442. GM_xmlhttpRequest(requestObj);
  443. }
  444.  
  445. function AsyncManager() {
  446. const AM = this;
  447.  
  448. // Ongoing xhr count
  449. this.taskCount = 0;
  450.  
  451. // Whether generate finish events
  452. let finishEvent = false;
  453. Object.defineProperty(this, 'finishEvent', {
  454. configurable: true,
  455. enumerable: true,
  456. get: () => (finishEvent),
  457. set: (b) => {
  458. finishEvent = b;
  459. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  460. }
  461. });
  462.  
  463. // Add one task
  464. this.add = () => (++AM.taskCount);
  465.  
  466. // Finish one task
  467. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  468. }
  469.  
  470. return [
  471. // Console & Debug
  472. LogLevel, DoLog, Err,
  473.  
  474. // DOM
  475. $, $All, $CrE, $AEL, $$CrE, addStyle, detectDom, destroyEvent,
  476.  
  477. // Data
  478. copyProp, copyProps, parseArgs, escJsStr, replaceText,
  479.  
  480. // Environment & Browser
  481. getUrlArgv, dl_browser, dl_GM,
  482.  
  483. // Logic & Task
  484. AsyncManager,
  485. ];
  486. })();