Basic Functions (For userscripts)

Useful functions for myself

Ce script ne doit pas être installé directement. C'est une librairie destinée à être incluse dans d'autres scripts avec la méta-directive // @require https://update.greatest.deepsurf.us/scripts/456034/1596651/Basic%20Functions%20%28For%20userscripts%29.js

  1. // ==UserScript==
  2. // @name Basic Functions (For userscripts)
  3. // @name:zh-CN 常用函数(用户脚本)
  4. // @name:en Basic Functions (For userscripts)
  5. // @namespace PY-DNG Userscripts
  6. // @version 1.9.3
  7. // @description Useful functions for myself
  8. // @description:zh-CN 自用函数
  9. // @description:en Useful functions for myself
  10. // @author PY-DNG
  11. // @license GPL-3.0-or-later
  12. // ==/UserScript==
  13.  
  14. /* eslint-disable no-multi-spaces */
  15. /* eslint-disable no-return-assign */
  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, FunctionLoader, loadFuncs, require, isLoaded
  35. ] = (function() {
  36. const [LogLevel, DoLog] = (function() {
  37. /**
  38. * level defination for DoLog function, bigger ones has higher possibility to be printed in console
  39. * @typedef {Object} LogLevel
  40. * @property {0} None - 0
  41. * @property {1} Error - 1
  42. * @property {2} Success - 2
  43. * @property {3} Warning - 3
  44. * @property {4} Info - 4
  45. */
  46. /** @type {LogLevel} */
  47. const LogLevel = {
  48. None: 0,
  49. Error: 1,
  50. Success: 2,
  51. Warning: 3,
  52. Info: 4,
  53. };
  54.  
  55. return [LogLevel, DoLog];
  56.  
  57. /**
  58. * @overload
  59. * @param {String} content - log content
  60. */
  61. /**
  62. * @overload
  63. * @param {Number} level - level specified in LogLevel object
  64. * @param {String} content - log content
  65. */
  66. /**
  67. * Logger with level and logger function specification
  68. * @overload
  69. * @param {Number} level - level specified in LogLevel object
  70. * @param {String} content - log content
  71. * @param {String} logger - which log function to use (in window.console[logger])
  72. */
  73. function DoLog() {
  74. // Get window
  75. const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;
  76.  
  77. const LogLevelMap = {};
  78. LogLevelMap[LogLevel.None] = {
  79. prefix: '',
  80. color: 'color:#ffffff'
  81. }
  82. LogLevelMap[LogLevel.Error] = {
  83. prefix: '[Error]',
  84. color: 'color:#ff0000'
  85. }
  86. LogLevelMap[LogLevel.Success] = {
  87. prefix: '[Success]',
  88. color: 'color:#00aa00'
  89. }
  90. LogLevelMap[LogLevel.Warning] = {
  91. prefix: '[Warning]',
  92. color: 'color:#ffa500'
  93. }
  94. LogLevelMap[LogLevel.Info] = {
  95. prefix: '[Info]',
  96. color: 'color:#888888'
  97. }
  98. LogLevelMap[LogLevel.Elements] = {
  99. prefix: '[Elements]',
  100. color: 'color:#000000'
  101. }
  102.  
  103. // Current log level
  104. DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  105.  
  106. // Log counter
  107. DoLog.logCount === undefined && (DoLog.logCount = 0);
  108.  
  109. // Get args
  110. let [level, logContent, logger] = parseArgs([...arguments], [
  111. [2],
  112. [1,2],
  113. [1,2,3]
  114. ], [LogLevel.Info, 'DoLog initialized.', 'log']);
  115.  
  116. let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : '');
  117. let subst = LogLevelMap[level].color;
  118.  
  119. switch (typeof(logContent)) {
  120. case 'string':
  121. msg += '%s';
  122. break;
  123. case 'number':
  124. msg += '%d';
  125. break;
  126. default:
  127. msg += '%o';
  128. break;
  129. }
  130.  
  131. // Log when log level permits
  132. if (level <= DoLog.logLevel) {
  133. // Log to console when log level permits
  134. if (level <= DoLog.logLevel) {
  135. if (++DoLog.logCount > 512) {
  136. console.clear();
  137. DoLog.logCount = 0;
  138. }
  139. console[logger](msg, subst, logContent);
  140. }
  141. }
  142. }
  143. }) ();
  144.  
  145. /**
  146. * Throw an error
  147. * @param {String} msg - the error message
  148. * @param {typeof Error} [ErrorConstructor=Error] - which error constructor to use, defaulting to Error()
  149. */
  150. function Err(msg, ErrorConstructor=Error) {
  151. throw new ErrorConstructor((typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + msg);
  152. }
  153.  
  154. /**
  155. * Assert given condition is true-like, otherwise throws given error
  156. * @param {*} condition
  157. * @param {string} errmsg
  158. * @param {typeof Error} [ErrorConstructor=Error]
  159. */
  160. function Assert(condition, errmsg, ErrorConstructor=Error) {
  161. condition || Err(errmsg, ErrorConstructor);
  162. }
  163.  
  164. /**
  165. * Convenient function to querySelector
  166. * @overload
  167. * @param {Element|Document|DocumentFragment} [root] - which target to call querySelector on
  168. * @param {string} selector - querySelector selector
  169. * @returns {Element|null}
  170. */
  171. function $() {
  172. switch(arguments.length) {
  173. case 2:
  174. return arguments[0].querySelector(arguments[1]);
  175. default:
  176. return document.querySelector(arguments[0]);
  177. }
  178. }
  179. /**
  180. * Convenient function to querySelectorAll
  181. * @overload
  182. * @param {Element|Document|DocumentFragment} [root] - which target to call querySelectorAll on
  183. * @param {string} selector - querySelectorAll selector
  184. * @returns {NodeList}
  185. */
  186. function $All() {
  187. switch(arguments.length) {
  188. case 2:
  189. return arguments[0].querySelectorAll(arguments[1]);
  190. break;
  191. default:
  192. return document.querySelectorAll(arguments[0]);
  193. }
  194. }
  195. /**
  196. * Convenient function to querySelectorAll
  197. * @overload
  198. * @param {Document} [root] - which document to call createElement on
  199. * @param {string} tagName
  200. * @returns {HTMLElement}
  201. */
  202. function $CrE() {
  203. switch(arguments.length) {
  204. case 2:
  205. return arguments[0].createElement(arguments[1]);
  206. break;
  207. default:
  208. return document.createElement(arguments[0]);
  209. }
  210. }
  211. /**
  212. * Convenient function to addEventListener
  213. * @overload
  214. * @param {EventTarget} target - which target to call addEventListener on
  215. * @param {string} type
  216. * @param {EventListenerOrEventListenerObject | null} callback
  217. * @param {AddEventListenerOptions | boolean} [options]
  218. */
  219. function $AEL(...args) {
  220. /** @type {EventTarget} */
  221. const target = args.shift();
  222. return target.addEventListener.apply(target, args);
  223. }
  224. /**
  225. * @typedef {[type: string, callback: EventListenerOrEventListenerObject | null, options: AddEventListenerOptions | boolean]} $AEL_Arguments
  226. */
  227. /**
  228. * @typedef {Object} $$CrE_Options
  229. * @property {string} tagName
  230. * @property {object} [props] - properties set by `element[prop] = value;`
  231. * @property {object} [attrs] - attributes set by `element.setAttribute(attr, value);`
  232. * @property {string | string[]} [classes] - class names to be set
  233. * @property {object} [styles] - styles set by `element[style_name] = style_value;`
  234. * @property {$AEL_Arguments[]} [listeners] - event listeners added by `$AEL(element, ...listener);`
  235. */
  236. /**
  237. * @overload
  238. * @param {$$CrE_Options} options
  239. * @returns {HTMLElement}
  240. */
  241. /**
  242. * Create configorated element
  243. * @overload
  244. * @param {string} tagName
  245. * @param {object} [props] - properties set by `element[prop] = value;`
  246. * @param {object} [attrs] - attributes set by `element.setAttribute(attr, value);`
  247. * @param {string | string[]} [classes] - class names to be set
  248. * @param {object} [styles] - styles set by `element[style_name] = style_value;`
  249. * @param {$AEL_Arguments[]} [listeners] - event listeners added by `$AEL(element, ...listener);`
  250. * @returns {HTMLElement}
  251. */
  252. function $$CrE() {
  253. const [tagName, props, attrs, classes, styles, listeners] = parseArgs([...arguments], [
  254. function(args, defaultValues) {
  255. const arg = args[0];
  256. return {
  257. 'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
  258. 'object': () => ['tagName', 'props', 'attrs', 'classes', 'styles', 'listeners'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
  259. }[typeof arg]();
  260. },
  261. [1,2],
  262. [1,2,3],
  263. [1,2,3,4],
  264. [1,2,3,4,5]
  265. ], ['div', {}, {}, [], {}, []]);
  266. const elm = $CrE(tagName);
  267. for (const [name, val] of Object.entries(props)) {
  268. elm[name] = val;
  269. }
  270. for (const [name, val] of Object.entries(attrs)) {
  271. elm.setAttribute(name, val);
  272. }
  273. for (const cls of Array.isArray(classes) ? classes : [classes]) {
  274. elm.classList.add(cls);
  275. }
  276. for (const [name, val] of Object.entries(styles)) {
  277. elm.style[name] = val;
  278. }
  279. for (const listener of listeners) {
  280. $AEL(elm, ...listener);
  281. }
  282. return elm;
  283. }
  284.  
  285. /**
  286. * @overload
  287. * @param {string} css - css content
  288. * @returns {HTMLStyleElement}
  289. */
  290. /**
  291. * @overload
  292. * @param {string} css - css content
  293. * @param {string} id - `id` attribute for <style> element
  294. * @returns {HTMLStyleElement}
  295. */
  296. /**
  297. * Append a style text to document(<head>) with a <style> element \
  298. * removes existing <style> elements with same id if id provided, so style updates can be done by using one same id
  299. *
  300. * Uses `GM_addElement` if `GM_addElement` exists and param `id` not specified. (`GM_addElement` uses id attribute, so specifing id manually when using `GM_addElement` takes no effect) \
  301. * In another case `GM_addStyle` instead of `GM_addElement` exists, and both `id` and `parentElement` not specified, `GM_addStyle` will be used. \
  302. * `document.createElement('style')` will be used otherwise.
  303. * @overload
  304. * @param {HTMLElement} parentElement - parent element to place <style> element
  305. * @param {string} css - css content
  306. * @param {string} id - `id` attribute for <style> element
  307. * @returns {HTMLStyleElement}
  308. */
  309. function addStyle() {
  310. // Get arguments
  311. const [parentElement, css, id] = parseArgs([...arguments], [
  312. [2],
  313. [2,3],
  314. [1,2,3]
  315. ], [null, '', null]);
  316.  
  317. if (typeof GM_addElement === 'function' && id === null) {
  318. return GM_addElement(parentElement, 'style', { textContent: css });
  319. } else if (typeof GM_addStyle === 'function' && parentElement === null && id === null) {
  320. return GM_addStyle(css);
  321. } else {
  322. // Make <style>
  323. const style = $CrE('style');
  324. style.innerHTML = css;
  325. id !== null && (style.id = id);
  326. id !== null && Array.from($All(`style#${id}`)).forEach(elm => elm.remove());
  327.  
  328. // Append to parentElement
  329. (parentElement ?? document.head).appendChild(style);
  330. return style;
  331. }
  332. }
  333.  
  334. /**
  335. * @typedef {Object} detectDom_options
  336. * @property {Node} root - root target to observe on
  337. * @property {string | string[]} [selector] - selector(s) to observe for, be aware that in options object it is named selector, but is named selectors in param
  338. * @property {boolean} [attributes] - whether to observe existing elements' attribute changes
  339. * @property {function} [callback] - if provided, use callback instead of Promise when selector element found
  340. */
  341. /**
  342. * @overload
  343. * @param {string | string[]} selector - selector(s) to observe for, be aware that in options object it is named selector, but is named selectors in param
  344. * @returns {Promise<HTMLElement>}
  345. */
  346. /**
  347. * @overload
  348. * @param {detectDom_options} options
  349. * @returns {MutationObserver}
  350. */
  351. /**
  352. * Get callback / resolve promise when specific dom/element appearce in document \
  353. * uses MutationObserver for implementation \
  354. * This behavior is different from versions that equals to or older than 0.8.4.2, so be careful when using it.
  355. * @overload
  356. * @param {Node} root - root target to observe on
  357. * @param {string | string[]} [selectors] - selector(s) to observe for
  358. * @param {boolean} [attributes] - whether to observe existing elements' attribute changes
  359. * @param {function} [callback] - if provided, use callback instead of Promise when selector element found
  360. * @returns {MutationObserver}
  361. */
  362. function detectDom() {
  363. let [selectors, root, attributes, callback] = parseArgs([...arguments], [
  364. function(args, defaultValues) {
  365. const arg = args[0];
  366. return {
  367. 'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
  368. 'object': () => ['selector', 'root', 'attributes', 'callback'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
  369. }[typeof arg]();
  370. },
  371. [2,1],
  372. [2,1,3],
  373. [2,1,3,4],
  374. ], [[''], document, false, null]);
  375. !Array.isArray(selectors) && (selectors = [selectors]);
  376.  
  377. if (select(root, selectors)) {
  378. for (const elm of selectAll(root, selectors)) {
  379. if (callback) {
  380. setTimeout(callback.bind(null, elm));
  381. } else {
  382. return Promise.resolve(elm);
  383. }
  384. }
  385. }
  386.  
  387. const observer = new MutationObserver(mCallback);
  388. observer.observe(root, {
  389. childList: true,
  390. subtree: true,
  391. attributes,
  392. });
  393.  
  394. let isPromise = !callback;
  395. return callback ? observer : new Promise((resolve, reject) => callback = resolve);
  396.  
  397. function mCallback(mutationList, observer) {
  398. const addedNodes = mutationList.reduce((an, mutation) => {
  399. switch (mutation.type) {
  400. case 'childList':
  401. an.push(...mutation.addedNodes);
  402. break;
  403. case 'attributes':
  404. an.push(mutation.target);
  405. break;
  406. }
  407. return an;
  408. }, []);
  409. const addedSelectorNodes = addedNodes.reduce((nodes, anode) => {
  410. if (anode.matches && match(anode, selectors)) {
  411. nodes.add(anode);
  412. }
  413. const childMatches = anode.querySelectorAll ? selectAll(anode, selectors) : [];
  414. for (const cm of childMatches) {
  415. nodes.add(cm);
  416. }
  417. return nodes;
  418. }, new Set());
  419. for (const node of addedSelectorNodes) {
  420. callback(node);
  421. isPromise && observer.disconnect();
  422. }
  423. }
  424.  
  425. function selectAll(elm, selectors) {
  426. !Array.isArray(selectors) && (selectors = [selectors]);
  427. return selectors.map(selector => [...$All(elm, selector)]).reduce((all, arr) => {
  428. all.push(...arr);
  429. return all;
  430. }, []);
  431. }
  432.  
  433. function select(elm, selectors) {
  434. const all = selectAll(elm, selectors);
  435. return all.length ? all[0] : null;
  436. }
  437.  
  438. function match(elm, selectors) {
  439. return !!elm.matches && selectors.some(selector => elm.matches(selector));
  440. }
  441. }
  442.  
  443. /**
  444. * Just stopPropagation and preventDefault
  445. * @param {Event} e
  446. */
  447. function destroyEvent(e) {
  448. if (!e) {return false;};
  449. if (!e instanceof Event) {return false;};
  450. e.stopPropagation();
  451. e.preventDefault();
  452. }
  453.  
  454. /**
  455. * copy property value from obj1 to obj2 if exists
  456. * @param {object} obj1
  457. * @param {object} obj2
  458. * @param {string|Symbol} prop
  459. */
  460. function copyProp(obj1, obj2, prop) {obj1.hasOwnProperty(prop) && (obj2[prop] = obj1[prop]);}
  461. /**
  462. * copy property values from obj1 to obj2 if exists
  463. * @param {object} obj1
  464. * @param {object} obj2
  465. * @param {string|Symbol} [props] - properties to copy, copy all enumerable properties if not specified
  466. */
  467. function copyProps(obj1, obj2, props) {(props ?? Object.keys(obj1)).forEach((prop) => (copyProp(obj1, obj2, prop)));}
  468.  
  469. /**
  470. * Argument parser with sorting and defaultValue support \
  471. * See use cases in other functions
  472. * @param {Array} args - original arguments' value to be parsed
  473. * @param {(number[]|function)[]} rules - rules to sort arguments or custom function to parse arguments
  474. * @param {Array} defaultValues - default values for arguments not provided a value
  475. * @returns {Array}
  476. */
  477. function parseArgs(args, rules, defaultValues=[]) {
  478. // args and rules should be array, but not just iterable (string is also iterable)
  479. if (!Array.isArray(args) || !Array.isArray(rules)) {
  480. throw new TypeError('parseArgs: args and rules should be array')
  481. }
  482.  
  483. // fill rules[0]
  484. (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);
  485.  
  486. // max arguments length
  487. const count = rules.length - 1;
  488.  
  489. // args.length must <= count
  490. if (args.length > count) {
  491. throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
  492. }
  493.  
  494. // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
  495. for (let i = 1; i <= count; i++) {
  496. const rule = rules[i];
  497. if (Array.isArray(rule)) {
  498. if (rule.length !== i) {
  499. throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
  500. }
  501. if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
  502. throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
  503. }
  504. } else if (typeof rule !== 'function') {
  505. throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
  506. }
  507. }
  508.  
  509. // Parse
  510. const rule = rules[args.length];
  511. let parsed;
  512. if (Array.isArray(rule)) {
  513. parsed = [...defaultValues];
  514. for (let i = 0; i < rule.length; i++) {
  515. parsed[rule[i]-1] = args[i];
  516. }
  517. } else {
  518. parsed = rule(args, defaultValues);
  519. }
  520. return parsed;
  521. }
  522.  
  523. /**
  524. * escape str into javascript written format
  525. * @param {string} str
  526. * @param {string} [quote]
  527. * @returns
  528. */
  529. function escJsStr(str, quote='"') {
  530. str = str.replaceAll('\\', '\\\\').replaceAll(quote, '\\' + quote).replaceAll('\t', '\\t');
  531. str = quote === '`' ? str.replaceAll(/(\$\{[^\}]*\})/g, '\\$1') : str.replaceAll('\r', '\\r').replaceAll('\n', '\\n');
  532. return quote + str + quote;
  533. }
  534. /**
  535. * Replace given text with no mismatching of replacing replaced text
  536. *
  537. * e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee' \
  538. * replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA' \
  539. * replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}' \
  540. * replaceText('abcd', {}) === 'abcd'
  541. *
  542. * **Note**: \
  543. * replaceText will replace in sort of replacer's iterating sort \
  544. * e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT' \
  545. * but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was \
  546. * not always the case, and the order is complex. As a result, it's best not to rely on property order. \
  547. * So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to \
  548. * replace irrelevance replacer keys only.
  549. * @param {string} text
  550. * @param {object} replacer
  551. * @returns {string}
  552. */
  553. function replaceText(text, replacer) {
  554. if (Object.entries(replacer).length === 0) {return text;}
  555. const [models, targets] = Object.entries(replacer);
  556. const len = models.length;
  557. let text_arr = [{text: text, replacable: true}];
  558. for (const [model, target] of Object.entries(replacer)) {
  559. text_arr = replace(text_arr, model, target);
  560. }
  561. return text_arr.map((text_obj) => (text_obj.text)).join('');
  562.  
  563. function replace(text_arr, model, target) {
  564. const result_arr = [];
  565. for (const text_obj of text_arr) {
  566. if (text_obj.replacable) {
  567. const splited = text_obj.text.split(model);
  568. for (const part of splited) {
  569. result_arr.push({text: part, replacable: true});
  570. result_arr.push({text: target, replacable: false});
  571. }
  572. result_arr.pop();
  573. } else {
  574. result_arr.push(text_obj);
  575. }
  576. }
  577. return result_arr;
  578. }
  579. }
  580.  
  581. /**
  582. * @typedef {Object} getUrlArgv_options
  583. * @property {string} name
  584. * @property {string} [url]
  585. * @property {string} [defaultValue]
  586. * @property {function} [dealFunc] - function that inputs original getUrlArgv result and outputs final return value
  587. */
  588. /**
  589. * @overload
  590. * @param {Object} getUrlArgv_options
  591. * @returns
  592. */
  593. /**
  594. * Get a url argument from location.href
  595. * @param {string} name
  596. * @param {string} [url]
  597. * @param {string} [defaultValue]
  598. * @param {function} [dealFunc] - function that inputs original getUrlArgv result and outputs final return value
  599. */
  600. function getUrlArgv() {
  601. const [name, url, defaultValue, dealFunc] = parseArgs([...arguments], [
  602. function(args, defaultValues) {
  603. const arg = args[0];
  604. return {
  605. 'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
  606. 'object': () => ['name', 'url', 'defaultValue', 'dealFunc'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
  607. }[typeof arg]();
  608. },
  609. [2,1],
  610. [2,1,3],
  611. [2,1,3,4]
  612. ], [null, location.href, null, a => a]);
  613.  
  614. if (name === null) { return null; }
  615.  
  616. const search = new URL(url).search;
  617. const objSearch = new URLSearchParams(search);
  618. const raw = objSearch.has(name) ? objSearch.get(name) : defaultValue;
  619. const argv = dealFunc(raw);
  620.  
  621. return argv;
  622. }
  623.  
  624. /**
  625. * download file from given url by simulating <a download="..." href=""></a> clicks \
  626. * a common use case is to download Blob objects as file from `URL.createObjectURL`
  627. * @param {string} url
  628. * @param {string} filename
  629. */
  630. function dl_browser(url, filename) {
  631. const a = document.createElement('a');
  632. a.href = url;
  633. a.download = filename;
  634. a.click();
  635. }
  636.  
  637. /**
  638. * File download function \
  639. * details looks like the detail of GM_xmlhttpRequest \
  640. * onload function will be called after file saved to disk
  641. * @param {object} details
  642. */
  643. function dl_GM(details) {
  644. if (!details.url || !details.name) {return false;};
  645.  
  646. // Configure request object
  647. const requestObj = {
  648. url: details.url,
  649. responseType: 'blob',
  650. onload: function(e) {
  651. // Save file
  652. dl_browser(URL.createObjectURL(e.response), details.name);
  653.  
  654. // onload callback
  655. details.onload ? details.onload(e) : function() {};
  656. }
  657. }
  658. if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;};
  659. if (details.onprogress ) {requestObj.onprogress = details.onprogress;};
  660. if (details.onerror ) {requestObj.onerror = details.onerror;};
  661. if (details.onabort ) {requestObj.onabort = details.onabort;};
  662. if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
  663. if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;};
  664.  
  665. // Send request
  666. Assert(typeof GM_xmlhttpRequest === 'function', 'GM_xmlhttpRequest should be provided in order to use dl_GM', TypeError);
  667. GM_xmlhttpRequest(requestObj);
  668. }
  669.  
  670. /**
  671. * Manager to manager async tasks \
  672. * This was written when I haven't learnt Promise, so for fluent promise users, just ignore it:)
  673. *
  674. * # Usage
  675. * ```javascript
  676. * // This simulates a async task, it can be a XMLHttpRequest, some file reading, or so on...
  677. * function someAsyncTask(callback, duration) {
  678. * const result = Math.random();
  679. * setTimeout(() => callback(result), duration);
  680. * }
  681. *
  682. * // Do 10 async tasks, and log all results when all async tasks finished
  683. * const AM = new AsyncManager();
  684. * const results = [];
  685. * AM.onfinish = function() {
  686. * console.log('All tasks finished!');
  687. * console.log(results);
  688. * }
  689. *
  690. * for (let i = 0; i < 10; i++) {
  691. * AM.add();
  692. * const duration = (Math.random() * 5 + 5) * 1000;
  693. * const index = i;
  694. * someAsyncTask(result => {
  695. * console.log(`Task ${index} finished after ${duration}ms!`);
  696. * results[index] = result;
  697. * }, duration);
  698. * console.log(`Task ${index} started!`);
  699. * }
  700. *
  701. * // Set AM.finishEvent to true after all tasks added, allowing AsyncManager to call onfinish callback
  702. * ```
  703. * @constructor
  704. */
  705. function AsyncManager() {
  706. const AM = this;
  707.  
  708. // Ongoing tasks count
  709. this.taskCount = 0;
  710.  
  711. // Whether generate finish events
  712. let finishEvent = false;
  713. Object.defineProperty(this, 'finishEvent', {
  714. configurable: true,
  715. enumerable: true,
  716. get: () => (finishEvent),
  717. set: (b) => {
  718. finishEvent = b;
  719. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  720. }
  721. });
  722.  
  723. // Add one task
  724. this.add = () => (++AM.taskCount);
  725.  
  726. // Finish one task
  727. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  728. }
  729.  
  730. /**
  731. * Put tasks in specific queue and order their execution \
  732. * Set `queueTask[queueId].max`, `queueTask[queueId].sleep` to custom queue's max ongoing tasks and sleep time between tasks
  733. * @param {function} task - task function to run
  734. * @param {string | Symbol} queueId - identifier to specify a target queue. if provided, given task will be added into specified queue.
  735. * @returns
  736. */
  737. function queueTask(task, queueId='default') {
  738. init();
  739.  
  740. return new Promise((resolve, reject) => {
  741. queueTask.hasOwnProperty(queueId) || (queueTask[queueId] = { tasks: [], ongoing: 0 });
  742. queueTask[queueId].tasks.push({task, resolve, reject});
  743. checkTask(queueId);
  744. });
  745.  
  746. function init() {
  747. if (!queueTask[queueId]?.initialized) {
  748. queueTask[queueId] = {
  749. // defaults
  750. tasks: [],
  751. ongoing: 0,
  752. max: 3,
  753. sleep: 500,
  754.  
  755. // user's pre-sets
  756. ...(queueTask[queueId] || {}),
  757.  
  758. // initialized flag
  759. initialized: true
  760. }
  761. };
  762. }
  763.  
  764. function checkTask() {
  765. const queue = queueTask[queueId];
  766. setTimeout(() => {
  767. if (queue.ongoing < queue.max && queue.tasks.length) {
  768. const task = queue.tasks.shift();
  769. queue.ongoing++;
  770. setTimeout(
  771. () => task.task().then(v => {
  772. queue.ongoing--;
  773. task.resolve(v);
  774. checkTask(queueId);
  775. }).catch(e => {
  776. queue.ongoing--;
  777. task.reject(e);
  778. checkTask(queueId);
  779. }),
  780. queue.sleep
  781. );
  782. }
  783. });
  784. }
  785. }
  786.  
  787. const [FunctionLoader, loadFuncs, require, isLoaded] = (function() {
  788. /**
  789. * 一般用作函数对象oFunc的加载条件,检测当前环境是否适合/需要该oFunc加载
  790. * @typedef {Object} checker_func
  791. * @property {string} type - checker's identifier
  792. * @property {function} func - actual internal judgement implementation
  793. */
  794. /**
  795. * 一般用作函数对象oFunc的加载条件,检测当前环境是否适合/需要该oFunc加载
  796. * @typedef {Object} checker
  797. * @property {string} type - checker's identifier
  798. * @property {*} value - param that goes into checker function
  799. */
  800. /**
  801. * 需要使用的substorage名称
  802. * @typedef {"GM_setValue" | "GM_getValue" | "GM_listValues" | "GM_deleteValue"} substorage_value
  803. */
  804. /**
  805. * 可以传入params的字符串名称
  806. * @typedef {'oFunc' | substorage_value} param
  807. */
  808. /**
  809. * 被加载函数对象的func函数
  810. * @callback oFuncBody
  811. * @param {oFunc} oFunc
  812. * @returns {*|Promise<*>}
  813. */
  814. /**
  815. * 被加载执行的函数对象
  816. * @typedef {Object} oFunc
  817. * @property {string} id - 每次load(每个FuncPool实例)内唯一的标识符
  818. * @property {boolean} [disabled] - 为真值时,无论checkers还是detectDom等任何其他条件通过或未通过,均不执行此函数对象;默认为false
  819. * @property {checker[]|checker} [checkers] - oFunc执行的条件
  820. * @property {string[]|string} [detectDom] - 如果提供,开始checker检查前会首先等待其中所有css选择器对应的元素在document中出现
  821. * @property {string[]|string} [dependencies] - 如果提供,应为其他函数对象的id或者id列表;开始checker检查前会首先等待其中所有指定的函数对象加载完毕
  822. * @property {boolean} [readonly] - 指定该函数的返回值是否应该被Proxy保护为不可修改对象
  823. * @property {param[]|param} params - 可选,指定传入oFunc.func的参数列表;可以为参数本身或其组成的数组
  824. * 参数可以为 字符串 或是 其他类型,如果是字符串就传入对应的FunctionLoader提供的内置值(见下),如果是其他类型则按照原样传入
  825. * - "oFunc":
  826. * 函数对象本身
  827. * - "GM_setValue", "GM_getValue", "GM_listValues", "GM_deleteValue":
  828. * 和脚本管理器提供的函数一致,但是读取和写入的对象是以oFunc.id为键的子空间
  829. * 比如,GM_getValue("prop") 就相当于调用脚本管理器提供的的 GM_getValue(oFunc.id)["prop"]
  830. * @property {oFuncBody} func - 实际实现了功能的函数
  831. * @property {boolean} [STOP] - [调试用] 指定不执行此函数对象
  832. */
  833.  
  834. const registered_checkers = {
  835. switch: value => value,
  836. url: value => location.href === value,
  837. path: value => location.pathname === value,
  838. regurl: value => !!location.href.match(value),
  839. regpath: value => !!location.pathname.match(value),
  840. starturl: value => location.href.startsWith(value),
  841. startpath: value => location.pathname.startsWith(value),
  842. func: value => value()
  843. };
  844.  
  845. class FuncPool extends EventTarget {
  846. static #STILL_LOADING = Symbol('oFunc still loading');
  847. static FunctionNotFound = Symbol('Function not found');
  848. static FunctionNotLoaded = Symbol('Function not loaded');
  849. static CheckerNotPass = Symbol('Function checker does not pass');
  850. static ErrorWhileLoad = Symbol('Error caught when function loading');
  851.  
  852. /** @typedef {symbol|*} return_value */
  853. /** @type {Map<oFunc, return_value>} */
  854. #oFuncs = new Map();
  855.  
  856. #GM_funcs;
  857.  
  858. /** @typedef {{error: Error, oFunc: oFunc}} load_error */
  859. /** @type {load_error[]} */
  860. errors;
  861.  
  862. /**
  863. * 创建新函数池
  864. * @param {Object} [details={}] - 可选,默认为{}空对象
  865. * @param {function} [details.GM_getValue] - 可选,读取脚本存储的函数;如果提供,使用提供的值,否则使用上下文中的值
  866. * @param {function} [details.GM_setValue] - 可选,写入脚本存储的函数;如果提供,使用提供的值,否则使用上下文中的值
  867. * @param {function} [details.GM_deleteValue] - 可选,删除脚本存储的函数;如果提供,使用提供的值,否则使用上下文中的值
  868. * @param {function} [details.GM_listValues] - 可选,列出脚本存储的函数;如果提供,使用提供的值,否则使用上下文中的值
  869. * @param {oFunc | oFunc[]} [oFuncs] - 可选,需要立即加载的函数对象
  870. * @returns {FuncPool}
  871. */
  872. constructor({
  873. GM_getValue: _GM_getValue = typeof GM_getValue === 'function' ? GM_getValue : null,
  874. GM_setValue: _GM_setValue = typeof GM_setValue === 'function' ? GM_setValue : null,
  875. GM_deleteValue: _GM_deleteValue = typeof GM_deleteValue === 'function' ? GM_deleteValue : null,
  876. GM_listValues: _GM_listValues = typeof GM_listValues === 'function' ? GM_listValues : null,
  877. oFuncs = []
  878. } = {}) {
  879. super();
  880. this.#GM_funcs = {
  881. GM_getValue: _GM_getValue,
  882. GM_setValue: _GM_setValue,
  883. GM_deleteValue: _GM_deleteValue,
  884. GM_listValues: _GM_listValues
  885. };
  886. this.errors = [];
  887. this.load(oFuncs);
  888. }
  889.  
  890. /**
  891. * 加载提供的一个或多个函数对象,并将其加入到函数池中 \
  892. * 异步函数,当所有传入的函数对象都彻底load完毕/checkers确定不加载时resolve
  893. * @param {oFunc[]|oFunc} [oFuncs] - 可选,需要加载的函数对象或其数组,不提供时默认为空数组
  894. */
  895. async load(oFuncs=[]) {
  896. oFuncs = Array.isArray(oFuncs) ? oFuncs : [oFuncs];
  897. await Promise.all(oFuncs.map(oFunc => this.#load(oFunc)));
  898. }
  899.  
  900. /**
  901. * 加载一个函数对象,并将其加入到函数池中 \
  902. * 当id重复时,直接报错RedeclarationError \
  903. * 异步函数,当彻底load完毕/checkers确定不加载时resolve \
  904. * 当加载完毕时,广播load事件;如果全部加载完毕,还广播all_load事件
  905. * @todo 当checker确定不加载时,广播什么事件?后续all_load是否仍然触发?
  906. * @param {oFunc} oFunc
  907. * @returns {Promise<boolean>} 本次调用是否成功执行了加载并顺利加载完毕
  908. */
  909. async #load(oFunc) {
  910. const that = this;
  911.  
  912. // disabled的函数对象,不执行
  913. if (oFunc.disabled) {
  914. return false;
  915. }
  916.  
  917. // 已经在函数池中的函数对象,不重复load
  918. if (this.#oFuncs.has(oFunc)) {
  919. return false;
  920. }
  921.  
  922. // 检查有无重复id
  923. for (const o of this.#oFuncs.keys()) {
  924. if (o.id === oFunc.id) {
  925. throw new RedeclarationError(`Attempts to load oFunc with id already in use: ${oFunc.id}`);
  926. }
  927. }
  928.  
  929. // 设置当前返回值为STILL_LOADING
  930. this.#oFuncs.set(oFunc, FuncPool.#STILL_LOADING);
  931.  
  932. // 加载依赖
  933. const dependencies = Array.isArray(oFunc.dependencies) ? oFunc.dependencies : ( oFunc.dependencies ? [oFunc.dependencies] : [] );
  934. await Promise.all(dependencies.map(id => new Promise((resolve, reject) => {
  935. $AEL(that, 'load', e => e.detail.oFunc.id === id && resolve());
  936. })));
  937.  
  938. // 检测checkers加载条件
  939. const checkers = Array.isArray(oFunc.checkers) ? oFunc.checkers : ( oFunc.checkers ? [oFunc.checkers] : [] );
  940. if (!testCheckers(checkers, oFunc)) {
  941. this.#oFuncs.set(oFunc, FuncPool.CheckerNotPass);
  942. return false;
  943. }
  944.  
  945. // 检测detectDOM中css选择器指定的元素出现
  946. const selectors = Array.isArray(oFunc.detectDom) ? oFunc.detectDom : ( oFunc.detectDom ? [oFunc.detectDom] : [] );
  947. await Promise.all(selectors.map(selector => detectDom(selector)));
  948.  
  949. // 处理substorage
  950. const substorage = this.#MakeSubStorage(oFunc.id);
  951.  
  952. // 处理函数参数
  953. const builtins = {
  954. oFunc,
  955. ...substorage
  956. };
  957. const params = oFunc.params ? (Array.isArray(oFunc.params) ? oFunc.params : [oFunc.params]) : [];
  958. const args = params.map(param => typeof param === 'string' ? builtins[param] : param);
  959.  
  960. // 执行函数对象
  961. let raw_return_value, return_value;
  962. try {
  963. raw_return_value = oFunc.func(...args);
  964. return_value = await Promise.resolve(raw_return_value);
  965. } catch (error) {
  966. // 当出现错误时,广播错误事件,储存错误信息,设置错误状态
  967. const load_error = { error, oFunc };
  968. this.#broadcast('error', load_error);
  969. this.errors.push(load_error);
  970. this.#oFuncs.set(oFunc, FuncPool.ErrorWhileLoad);
  971. return false;
  972. }
  973.  
  974. // 设置返回值
  975. this.#oFuncs.set(oFunc, return_value);
  976.  
  977. // 广播事件
  978. this.#broadcast('load', { oFunc, id: oFunc.id, return_value });
  979. Array.from(this.#oFuncs.values()).every(v => v !== FuncPool.#STILL_LOADING) &&
  980. this.#broadcast('all_load');
  981. return true;
  982. }
  983.  
  984. /**
  985. * 获取指定函数对象的返回值 \
  986. * 如果指定的函数对象不存在,返回FuncPool.FunctionNotFound \
  987. * 如果指定的函数对象存在但尚未加载,返回FuncPool.FunctionNotLoaded \
  988. * 如果函数对象指定了readonly为真值,则返回前用Proxy包装返回值,使其不可修改
  989. * @param {string} id - 函数对象的id
  990. * @returns {*}
  991. */
  992. require(id) {
  993. for (const [oFunc, return_value] of this.#oFuncs.entries()) {
  994. if (oFunc.id === id) {
  995. if (return_value === FuncPool.#STILL_LOADING) {
  996. return FuncPool.FunctionNotLoaded;
  997. } else {
  998. return oFunc.readonly ? FuncPool.#MakeReadonlyObj(return_value) : return_value;
  999. }
  1000. }
  1001. }
  1002. return FuncPool.FunctionNotFound;
  1003. }
  1004.  
  1005. isLoaded(id) {
  1006. for (const [oFunc, return_value] of this.#oFuncs.entries()) {
  1007. if (oFunc.id === id) {
  1008. if (return_value === FuncPool.#STILL_LOADING) {
  1009. return false;
  1010. } else {
  1011. return true;
  1012. }
  1013. }
  1014. return false;
  1015. }
  1016. }
  1017.  
  1018. /**
  1019. * 调用this.dispatchEvent分发自定义事件
  1020. * 同时对可分发的事件名称进行限制
  1021. * @param {'load' | 'all_load' | 'error'} evt_name
  1022. * @param {*} [detail]
  1023. */
  1024. #broadcast(evt_name, detail) {
  1025. return this.dispatchEvent(new CustomEvent(evt_name, { detail }));
  1026. }
  1027.  
  1028. get GM_funcs() {
  1029. return { ...this.#GM_funcs };
  1030. }
  1031.  
  1032. /**
  1033. * 以Proxy包装value,使其属性只读 \
  1034. * 如果传入的不是object,则直接返回value \
  1035. * @param {Object} val
  1036. * @returns {Proxy}
  1037. */
  1038. static #MakeReadonlyObj(val) {
  1039. return isObject(val) ? new Proxy(val, {
  1040. get: function(target, property, receiver) {
  1041. return FuncPool.#MakeReadonlyObj(target[property]);
  1042. },
  1043. set: function(target, property, value, receiver) {},
  1044. has: function(target, prop) {},
  1045. setPrototypeOf(target, newProto) {
  1046. return false;
  1047. },
  1048. defineProperty(target, property, descriptor) {
  1049. return true;
  1050. },
  1051. deleteProperty(target, property) {
  1052. return false;
  1053. },
  1054. preventExtensions(target) {
  1055. return false;
  1056. }
  1057. }) : val;
  1058.  
  1059. function isObject(value) {
  1060. return ['object', 'function'].includes(typeof value) && value !== null;
  1061. }
  1062. }
  1063.  
  1064. /**
  1065. * 创建适用于子功能函数的 GM_setValue, GM_getValue, GM_deleteValue 和 GM_listValues \
  1066. * 调用返回的`GM_setValue(str, val)`相当于对脚本管理器提供的GM*函数进行如下调用:
  1067. * ``` javascript
  1068. * const obj = GM_getValue(key, {});
  1069. * if (typeof obj !== 'object' or obj === null) { throw new TypeError(''); }
  1070. * obj[str] = val;
  1071. * GM_setValue(key, obj);
  1072. * ```
  1073. * @param {string} key - 实际调用用户脚本管理器的GM*函数时提供的key,一般是子功能函数id
  1074. * @returns {{ GM_setValue: function, GM_getValue: function, GM_deleteValue: function, GM_listValues: function }}
  1075. */
  1076. #MakeSubStorage(key) {
  1077. const GM_funcs = this.#GM_funcs;
  1078. return {
  1079. GM_setValue(name, val) {
  1080. checkGrant(['GM_setValue', 'GM_getValue'], 'GM_setValue');
  1081. const obj = GM_funcs.GM_getValue(key, {});
  1082. Assert(isObject(obj), `FunctionLoader: storage item of key ${name} should be an object`, TypeError);
  1083. obj[name] = val;
  1084. GM_funcs.GM_setValue(key, obj);
  1085. },
  1086. GM_getValue(name, default_value=null) {
  1087. checkGrant(['GM_getValue'], 'GM_getValue');
  1088. const obj = GM_funcs.GM_getValue(key, {});
  1089. return obj.hasOwnProperty(name) ? obj[name] : default_value;
  1090. },
  1091. GM_deleteValue(name) {
  1092. checkGrant(['GM_setValue', 'GM_getValue'], 'GM_deleteValue');
  1093. const obj = GM_funcs.GM_getValue(key, {});
  1094. delete obj[name];
  1095. GM_funcs.GM_setValue(key, obj);
  1096. },
  1097. GM_listValues() {
  1098. checkGrant(['GM_getValue'], 'GM_listValues');
  1099. const obj = GM_funcs.GM_getValue(key, {});
  1100. return Object.keys(obj);
  1101. }
  1102. };
  1103.  
  1104. /**
  1105. * 检查指定的GM_*函数是否存在,不存在就抛出错误
  1106. * @param {string|string[]} funcnames
  1107. * @param {string} calling - 正在调用的GM_函数的名字,输出错误信息时用
  1108. */
  1109. function checkGrant(funcnames, calling) {
  1110. Array.isArray(funcnames) || (funcnames = [funcnames]);
  1111. for (const funcname of funcnames) {
  1112. Assert(GM_funcs[funcname], `FunctionLoader: @grant ${funcname} in userscript metadata before using ${calling}`, TypeError);
  1113. }
  1114. }
  1115.  
  1116. function isObject(val) {
  1117. return typeof val === 'object' && val !== null;
  1118. }
  1119. }
  1120. }
  1121. class RedeclarationError extends TypeError {}
  1122. class CircularDependencyError extends ReferenceError {}
  1123.  
  1124.  
  1125. // 预置的函数池
  1126. const default_pool = new FuncPool();
  1127.  
  1128. /**
  1129. * 在预置的函数池中加载函数对象或其数组
  1130. * @param {oFunc[]|oFunc} oFuncs - 需要执行的函数对象
  1131. * @returns {FuncPool}
  1132. */
  1133. function loadFuncs(oFuncs) {
  1134. default_pool.load(oFuncs);
  1135. return default_pool;
  1136. }
  1137.  
  1138. /**
  1139. * 在预置的函数池中获取函数对象的返回值
  1140. * @param {string} id - 函数对象的字符串id
  1141. * @returns {*}
  1142. */
  1143. function require(id) {
  1144. return default_pool.require(id);
  1145. }
  1146.  
  1147. /**
  1148. * 在预置的函数池中检查指定函数对象是否已经加载完毕(有返回值可用)
  1149. * @param {string} id - 函数对象的字符串id
  1150. * @returns {boolean}
  1151. */
  1152. function isLoaded(id) {
  1153. return default_pool.isLoaded(id);
  1154. }
  1155.  
  1156. /**
  1157. * 测试给定checker是否检测通过 \
  1158. * 给定多个checker时,checkers之间是 或 关系,有一个checker通过即算作整体通过 \
  1159. * 注意此函数设计和旧版testChecker的设计不同,旧版中一个checker可以有多个值,还可通过checker.all指定多值之间的关系为 与 还是 或
  1160. * @param {checker[]|checker} [checkers] - 需要检测的checkers
  1161. * @param {oFunc|*} [this_value] - 如提供,将用作checkers运行时的this值;一般而言为checkers所属的函数对象
  1162. * @returns {boolean}
  1163. */
  1164. function testCheckers(checkers=[], this_value=null) {
  1165. checkers = Array.isArray(checkers) ? checkers : [checkers];
  1166. return checkers.length === 0 || checkers.some(checker => !!registered_checkers[checker.type]?.call(this_value, checker.value));
  1167. }
  1168.  
  1169. /**
  1170. * 注册新checker \
  1171. * 如果给定type已经被其他checker占用,则会报错RedeclarationError \
  1172. * @param {string} type - checker类名
  1173. * @param {function} func - checker implementation
  1174. */
  1175. function registerChecker(type, func) {
  1176. if (registered_checkers.hasOwnProperty(type)) {
  1177. throw RedeclarationError(`Attempts to register checker with type already in use: ${type}`);
  1178. }
  1179. registered_checkers[type] = func;
  1180. }
  1181.  
  1182. const FunctionLoader = {
  1183. FuncPool,
  1184. testCheckers,
  1185. registerChecker,
  1186. get checkers() {
  1187. return Object.assign({}, registered_checkers);
  1188. },
  1189. Error: {
  1190. RedeclarationError,
  1191. CircularDependencyError
  1192. }
  1193. };
  1194. return [FunctionLoader, loadFuncs, require, isLoaded];
  1195. }) ();
  1196.  
  1197. return [
  1198. // Console & Debug
  1199. LogLevel, DoLog, Err, Assert,
  1200.  
  1201. // DOM
  1202. $, $All, $CrE, $AEL, $$CrE, addStyle, detectDom, destroyEvent,
  1203.  
  1204. // Data
  1205. copyProp, copyProps, parseArgs, escJsStr, replaceText,
  1206.  
  1207. // Environment & Browser
  1208. getUrlArgv, dl_browser, dl_GM,
  1209.  
  1210. // Logic & Task
  1211. AsyncManager, queueTask, FunctionLoader, loadFuncs, require, isLoaded
  1212. ];
  1213. }) ();