常用函数(用户脚本)

自用函数

目前为 2023-12-29 提交的版本。查看 最新版本

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