Greasy Fork is available in English.

Basic Functions (For userscripts)

Useful functions for myself

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

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