Basic Functions

自用函数

Questo script non dovrebbe essere installato direttamente. È una libreria per altri script da includere con la chiave // @require https://update.greatest.deepsurf.us/scripts/449412/1122880/Basic%20Functions.js

  1. /* eslint-disable no-multi-spaces */
  2.  
  3. // ==UserScript==
  4. // @name Basic Functions
  5. // @name:zh-CN 常用函数
  6. // @name:en Basic Functions
  7. // @namespace Wenku8++
  8. // @version 0.8
  9. // @description 自用函数 For wenku8++
  10. // @description:zh-CN 自用函数 For wenku8++
  11. // @description:en Useful functions for myself
  12. // @author PY-DNG
  13. // @license GPL-license
  14. // @grant GM_info
  15. // @grant GM_addStyle
  16. // @grant GM_addElement
  17. // @grant GM_deleteValue
  18. // @grant GM_listValues
  19. // @grant GM_addValueChangeListener
  20. // @grant GM_removeValueChangeListener
  21. // @grant GM_setValue
  22. // @grant GM_getValue
  23. // @grant GM_log
  24. // @grant GM_getResourceText
  25. // @grant GM_getResourceURL
  26. // @grant GM_registerMenuCommand
  27. // @grant GM_unregisterMenuCommand
  28. // @grant GM_openInTab
  29. // @grant GM_xmlhttpRequest
  30. // @grant GM_download
  31. // @grant GM_getTab
  32. // @grant GM_saveTab
  33. // @grant GM_getTabs
  34. // @grant GM_notification
  35. // @grant GM_setClipboard
  36. // @grant GM_info
  37. // @grant unsafeWindow
  38. // ==/UserScript==
  39.  
  40. const LogLevel = {
  41. None: 0,
  42. Error: 1,
  43. Success: 2,
  44. Warning: 3,
  45. Info: 4,
  46. }
  47.  
  48. // Arguments: level=LogLevel.Info, logContent, trace=false
  49. // Needs one call "DoLog();" to get it initialized before using it!
  50. function DoLog() {
  51. // Get window
  52. const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;
  53.  
  54. const LogLevelMap = {};
  55. LogLevelMap[LogLevel.None] = {
  56. prefix: '',
  57. color: 'color:#ffffff'
  58. }
  59. LogLevelMap[LogLevel.Error] = {
  60. prefix: '[Error]',
  61. color: 'color:#ff0000'
  62. }
  63. LogLevelMap[LogLevel.Success] = {
  64. prefix: '[Success]',
  65. color: 'color:#00aa00'
  66. }
  67. LogLevelMap[LogLevel.Warning] = {
  68. prefix: '[Warning]',
  69. color: 'color:#ffa500'
  70. }
  71. LogLevelMap[LogLevel.Info] = {
  72. prefix: '[Info]',
  73. color: 'color:#888888'
  74. }
  75. LogLevelMap[LogLevel.Elements] = {
  76. prefix: '[Elements]',
  77. color: 'color:#000000'
  78. }
  79.  
  80. // Current log level
  81. DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  82.  
  83. // Log counter
  84. DoLog.logCount === undefined && (DoLog.logCount = 0);
  85.  
  86. // Get args
  87. let [level, logContent, trace] = parseArgs([...arguments], [
  88. [2],
  89. [1,2],
  90. [1,2,3]
  91. ], [LogLevel.Info, 'DoLog initialized.', false]);
  92.  
  93. // Log when log level permits
  94. if (level <= DoLog.logLevel) {
  95. let msg = '%c' + LogLevelMap[level].prefix + (typeof MODULE_DATA === 'object' ? '[' + MODULE_DATA.name + ']' : '') + (LogLevelMap[level].prefix ? ' ' : '');
  96. let subst = LogLevelMap[level].color;
  97.  
  98. switch (typeof(logContent)) {
  99. case 'string':
  100. msg += '%s';
  101. break;
  102. case 'number':
  103. msg += '%d';
  104. break;
  105. case 'object':
  106. msg += '%o';
  107. break;
  108. }
  109.  
  110. if (++DoLog.logCount > 512) {
  111. console.clear();
  112. DoLog.logCount = 0;
  113. }
  114. console[trace ? 'trace' : 'log'](msg, subst, logContent);
  115. }
  116. }
  117. DoLog();
  118.  
  119. // Basic functions
  120. // querySelector
  121. function $() {
  122. switch (arguments.length) {
  123. case 2:
  124. return arguments[0].querySelector(arguments[1]);
  125. break;
  126. default:
  127. return document.querySelector(arguments[0]);
  128. }
  129. }
  130. // querySelectorAll
  131. function $All() {
  132. switch (arguments.length) {
  133. case 2:
  134. return arguments[0].querySelectorAll(arguments[1]);
  135. break;
  136. default:
  137. return document.querySelectorAll(arguments[0]);
  138. }
  139. }
  140. // createElement
  141. function $CrE() {
  142. switch (arguments.length) {
  143. case 2:
  144. return arguments[0].createElement(arguments[1]);
  145. break;
  146. default:
  147. return document.createElement(arguments[0]);
  148. }
  149. }
  150. // addEventListener
  151. function $AEL(...args) {
  152. const target = args.shift();
  153. return target.addEventListener.apply(target, args);
  154. }
  155. // Object1[prop] ==> Object2[prop]
  156. function copyProp(obj1, obj2, prop) {
  157. obj1.hasOwnProperty(prop) && (obj2[prop] = obj1[prop]);
  158. }
  159. function copyProps(obj1, obj2, props) {
  160. (props || Object.keys(obj1)).forEach((prop) => (copyProp(obj1, obj2, prop)));
  161. }
  162.  
  163. function clearChildNodes(elm) {
  164. for (const el of elm.childNodes) {
  165. elm.removeChild(el);
  166. }
  167. }
  168.  
  169. // Just stopPropagation and preventDefault
  170. function destroyEvent(e) {
  171. if (!e) {
  172. return false;
  173. };
  174. if (!e instanceof Event) {
  175. return false;
  176. };
  177. e.stopPropagation();
  178. e.preventDefault();
  179. }
  180.  
  181. // GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
  182. // Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
  183. // (If the request is invalid, such as url === '', will return false and will NOT make this request)
  184. // If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
  185. // Requires: function delItem(){...} & function uniqueIDMaker(){...}
  186. function GMXHRHook(maxXHR = 5) {
  187. const GM_XHR = GM_xmlhttpRequest;
  188. const getID = uniqueIDMaker();
  189. let todoList = [],
  190. ongoingList = [];
  191. GM_xmlhttpRequest = safeGMxhr;
  192.  
  193. function safeGMxhr() {
  194. // Get an id for this request, arrange a request object for it.
  195. const id = getID();
  196. const request = {
  197. id: id,
  198. args: arguments,
  199. aborter: null
  200. };
  201.  
  202. // Deal onload function first
  203. dealEndingEvents(request);
  204.  
  205. /* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
  206. // Stop invalid requests
  207. if (!validCheck(request)) {
  208. return false;
  209. }
  210. */
  211.  
  212. // Judge if we could start the request now or later?
  213. todoList.push(request);
  214. checkXHR();
  215. return makeAbortFunc(id);
  216.  
  217. // Decrease activeXHRCount while GM_XHR onload;
  218. function dealEndingEvents(request) {
  219. const e = request.args[0];
  220.  
  221. // onload event
  222. const oriOnload = e.onload;
  223. e.onload = function() {
  224. reqFinish(request.id);
  225. checkXHR();
  226. oriOnload ? oriOnload.apply(null, arguments) : function() {};
  227. }
  228.  
  229. // onerror event
  230. const oriOnerror = e.onerror;
  231. e.onerror = function() {
  232. reqFinish(request.id);
  233. checkXHR();
  234. oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
  235. }
  236.  
  237. // ontimeout event
  238. const oriOntimeout = e.ontimeout;
  239. e.ontimeout = function() {
  240. reqFinish(request.id);
  241. checkXHR();
  242. oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
  243. }
  244.  
  245. // onabort event
  246. const oriOnabort = e.onabort;
  247. e.onabort = function() {
  248. reqFinish(request.id);
  249. checkXHR();
  250. oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
  251. }
  252. }
  253.  
  254. // Check if the request is invalid
  255. function validCheck(request) {
  256. const e = request.args[0];
  257.  
  258. if (!e.url) {
  259. return false;
  260. }
  261.  
  262. return true;
  263. }
  264.  
  265. // Call a XHR from todoList and push the request object to ongoingList if called
  266. function checkXHR() {
  267. if (ongoingList.length >= maxXHR) {
  268. return false;
  269. };
  270. if (todoList.length === 0) {
  271. return false;
  272. };
  273. const req = todoList.shift();
  274. const reqArgs = req.args;
  275. const aborter = GM_XHR.apply(null, reqArgs);
  276. req.aborter = aborter;
  277. ongoingList.push(req);
  278. return req;
  279. }
  280.  
  281. // Make a function that aborts a certain request
  282. function makeAbortFunc(id) {
  283. return function() {
  284. let i;
  285.  
  286. // Check if the request haven't been called
  287. for (i = 0; i < todoList.length; i++) {
  288. const req = todoList[i];
  289. if (req.id === id) {
  290. // found this request: haven't been called
  291. delItem(todoList, i);
  292. return true;
  293. }
  294. }
  295.  
  296. // Check if the request is running now
  297. for (i = 0; i < ongoingList.length; i++) {
  298. const req = todoList[i];
  299. if (req.id === id) {
  300. // found this request: running now
  301. req.aborter();
  302. reqFinish(id);
  303. checkXHR();
  304. }
  305. }
  306.  
  307. // Oh no, this request is already finished...
  308. return false;
  309. }
  310. }
  311.  
  312. // Remove a certain request from ongoingList
  313. function reqFinish(id) {
  314. let i;
  315. for (i = 0; i < ongoingList.length; i++) {
  316. const req = ongoingList[i];
  317. if (req.id === id) {
  318. ongoingList = delItem(ongoingList, i);
  319. return true;
  320. }
  321. }
  322. return false;
  323. }
  324. }
  325. }
  326.  
  327. // Get a url argument from lacation.href
  328. // also recieve a function to deal the matched string
  329. // returns defaultValue if name not found
  330. // Args: {url=location.href, name, dealFunc=((a)=>{return a;}), defaultValue=null} or 'name'
  331. function getUrlArgv(details) {
  332. typeof(details) === 'string' && (details = {
  333. name: details
  334. });
  335. typeof(details) === 'undefined' && (details = {});
  336. if (!details.name) {
  337. return null;
  338. };
  339.  
  340. const url = details.url ? details.url : location.href;
  341. const name = details.name ? details.name : '';
  342. const dealFunc = details.dealFunc ? details.dealFunc : ((a) => {
  343. return a;
  344. });
  345. const defaultValue = details.defaultValue ? details.defaultValue : null;
  346. const matcher = new RegExp('[\\?&]' + name + '=([^&#]+)');
  347. const result = url.match(matcher);
  348. const argv = result ? dealFunc(result[1]) : defaultValue;
  349.  
  350. return argv;
  351. }
  352.  
  353. // Append a style text to document(<head>) with a <style> element
  354. function addStyle(css, id) {
  355. const style = document.createElement("style");
  356. id && (style.id = id);
  357. style.textContent = css;
  358. for (const elm of $All(document, '#' + id)) {
  359. elm.parentElement && elm.parentElement.removeChild(elm);
  360. }
  361. document.head.appendChild(style);
  362. }
  363.  
  364. // Save dataURL to file
  365. function saveFile(dataURL, filename) {
  366. const a = document.createElement('a');
  367. a.href = dataURL;
  368. a.download = filename;
  369. a.click();
  370. }
  371.  
  372. // File download function
  373. // details looks like the detail of GM_xmlhttpRequest
  374. // onload function will be called after file saved to disk
  375. function downloadFile(details) {
  376. if (!details.url || !details.name) {
  377. return false;
  378. };
  379.  
  380. // Configure request object
  381. const requestObj = {
  382. url: details.url,
  383. responseType: 'blob',
  384. onload: function(e) {
  385. // Save file
  386. saveFile(URL.createObjectURL(e.response), details.name);
  387.  
  388. // onload callback
  389. details.onload ? details.onload(e) : function() {};
  390. }
  391. }
  392. if (details.onloadstart) {
  393. requestObj.onloadstart = details.onloadstart;
  394. };
  395. if (details.onprogress) {
  396. requestObj.onprogress = details.onprogress;
  397. };
  398. if (details.onerror) {
  399. requestObj.onerror = details.onerror;
  400. };
  401. if (details.onabort) {
  402. requestObj.onabort = details.onabort;
  403. };
  404. if (details.onreadystatechange) {
  405. requestObj.onreadystatechange = details.onreadystatechange;
  406. };
  407. if (details.ontimeout) {
  408. requestObj.ontimeout = details.ontimeout;
  409. };
  410.  
  411. // Send request
  412. GM_xmlhttpRequest(requestObj);
  413. }
  414.  
  415. // get '/' splited API array from a url
  416. function getAPI(url = location.href) {
  417. return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
  418. }
  419.  
  420. // get host part from a url(includes '^https://', '/$')
  421. function getHost(url = location.href) {
  422. const match = location.href.match(/https?:\/\/[^\/]+\//);
  423. return match ? match[0] : match;
  424. }
  425.  
  426. function AsyncManager() {
  427. const AM = this;
  428.  
  429. // Ongoing xhr count
  430. this.taskCount = 0;
  431.  
  432. // Whether generate finish events
  433. let finishEvent = false;
  434. Object.defineProperty(this, 'finishEvent', {
  435. configurable: true,
  436. enumerable: true,
  437. get: () => (finishEvent),
  438. set: (b) => {
  439. finishEvent = b;
  440. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  441. }
  442. });
  443.  
  444. // Add one task
  445. this.add = () => (++AM.taskCount);
  446.  
  447. // Finish one task
  448. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  449. }
  450.  
  451. // Polyfill String.prototype.replaceAll
  452. // replaceValue does NOT support regexp match groups($1, $2, etc.)
  453. function polyfill_replaceAll() {
  454. String.prototype.replaceAll = String.prototype.replaceAll ? String.prototype.replaceAll : PF_replaceAll;
  455.  
  456. function PF_replaceAll(searchValue, replaceValue) {
  457. const str = String(this);
  458.  
  459. if (searchValue instanceof RegExp) {
  460. const global = RegExp(searchValue, 'g');
  461. if (/\$/.test(replaceValue)) {
  462. console.error('Error: Polyfilled String.protopype.replaceAll does support regexp groups');
  463. };
  464. return str.replace(global, replaceValue);
  465. } else {
  466. return str.split(searchValue).join(replaceValue);
  467. }
  468. }
  469. }
  470.  
  471. function randint(min, max) {
  472. return Math.floor(Math.random() * (max - min + 1)) + min;
  473. }
  474.  
  475. // Replace model text with no mismatching of replacing replaced text
  476. // e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee'
  477. // replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA'
  478. // replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}'
  479. // replaceText('abcd', {}) === 'abcd'
  480. /* Note:
  481. replaceText will replace in sort of replacer's iterating sort
  482. e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT'
  483. but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was
  484. not always the case, and the order is complex. As a result, it's best not to rely on property order.
  485. So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to
  486. replace irrelevance replacer keys only.
  487. */
  488. function replaceText(text, replacer) {
  489. if (Object.entries(replacer).length === 0) {return text;}
  490. const [models, targets] = Object.entries(replacer);
  491. const len = models.length;
  492. let text_arr = [{text: text, replacable: true}];
  493. for (const [model, target] of Object.entries(replacer)) {
  494. text_arr = replace(text_arr, model, target);
  495. }
  496. return text_arr.map((text_obj) => (text_obj.text)).join('');
  497.  
  498. function replace(text_arr, model, target) {
  499. const result_arr = [];
  500. for (const text_obj of text_arr) {
  501. if (text_obj.replacable) {
  502. const splited = text_obj.text.split(model);
  503. for (const part of splited) {
  504. result_arr.push({text: part, replacable: true});
  505. result_arr.push({text: target, replacable: false});
  506. }
  507. result_arr.pop();
  508. } else {
  509. result_arr.push(text_obj);
  510. }
  511. }
  512. return result_arr;
  513. }
  514. }
  515.  
  516. // escape str into javascript written format
  517. function escJsStr(str, quote='"') {
  518. str = str.replaceAll('\\', '\\\\').replaceAll(quote, '\\' + quote);
  519. quote === '`' && (str = str.replaceAll(/(\$\{[^\}]*\})/g, '\\$1'));
  520. return quote + str + quote;
  521. }
  522.  
  523. function parseArgs(args, rules, defaultValues=[]) {
  524. // args and rules should be array, but not just iterable (string is also iterable)
  525. if (!Array.isArray(args) || !Array.isArray(rules)) {
  526. throw new TypeError('parseArgs: args and rules should be array')
  527. }
  528.  
  529. // fill rules[0]
  530. (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);
  531.  
  532. // max arguments length
  533. const count = rules.length - 1;
  534.  
  535. // args.length must <= count
  536. if (args.length > count) {
  537. throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
  538. }
  539.  
  540. // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
  541. for (let i = 1; i <= count; i++) {
  542. const rule = rules[i];
  543. if (Array.isArray(rule)) {
  544. if (rule.length !== i) {
  545. throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
  546. }
  547. if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
  548. throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
  549. }
  550. } else if (typeof rule !== 'function') {
  551. throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
  552. }
  553. }
  554.  
  555. // Parse
  556. const rule = rules[args.length];
  557. let parsed;
  558. if (Array.isArray(rule)) {
  559. parsed = [...defaultValues];
  560. for (let i = 0; i < rule.length; i++) {
  561. parsed[rule[i]-1] = args[i];
  562. }
  563. } else {
  564. parsed = rule(args, defaultValues);
  565. }
  566. return parsed;
  567. }
  568.  
  569. // Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
  570. function delItem(arr, delIndex) {
  571. arr = arr.slice(0, delIndex).concat(arr.slice(delIndex + 1));
  572. return arr;
  573. }
  574.  
  575. // type: [Error, TypeError]
  576. function Err(msg, type=0) {
  577. throw new [Error, TypeError][type]((typeof MODULE_DATA === 'object' ? '[' + MODULE_DATA.name + ']' : '') + msg);
  578. }