Userscript Utils

Some useful utilities for userscript development

目前为 2019-11-01 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/391648/745792/Userscript%20Utils.js

  1. // Userscript Utils library
  2. //
  3. // Some useful utilities for userscript development.
  4. //
  5. // https://greasyfork.org/scripts/391648-userscript-utils
  6. // Copyright (C) 2019, Guido Villa
  7. //
  8. // For information/instructions on user scripts, see:
  9. // https://greasyfork.org/help/installing-user-scripts
  10. //
  11. // To use this library in a userscript you must add to script header:
  12. // @require https://greasyfork.org/scripts/391648/code/userscript-utils.js
  13. // @grant GM_xmlhttpRequest (only if using UU.GM_xhR)
  14. // @grant GM_getValue (only if using UU.GM_getObject)
  15. // @grant GM_setValue (only if using UU.GM_setObject)
  16. // @grant GM_deleteValue (only if using UU.GM_deleteObject)
  17. //
  18. // --------------------------------------------------------------------
  19. //
  20. // ==UserScript==
  21. // @namespace https://greasyfork.org/users/373199-guido-villa
  22. // @exclude *
  23. //
  24. // ==UserLibrary==
  25. // @name Userscript Utils
  26. // @description Some useful utilities for userscript development
  27. // @version 1.1
  28. // @author guidovilla
  29. // @date 01.11.2019
  30. // @copyright 2019, Guido Villa (https://greasyfork.org/users/373199-guido-villa)
  31. // @license GPL-3.0-or-later
  32. // @homepageURL https://greasyfork.org/scripts/391648-userscript-utils
  33. // @supportURL https://gitlab.com/gv-browser/userscripts/issues
  34. // @contributionURL https://tinyurl.com/gv-donate-99
  35. // @attribution Trevor Dixon (https://stackoverflow.com/users/711902/trevor-dixon)
  36. // ==/UserScript==
  37. //
  38. // ==/UserLibrary==
  39. //
  40. // --------------------------------------------------------------------
  41. //
  42. // To-do (priority: [H]igh, [M]edium, [L]ow):
  43. // - [H] GM_xhR: remove workaround responseXML2 and make responseXML work
  44. // - [M] make a strict RFC 4180 compliant CSV parse method version
  45. // - [M] add other functions
  46. //
  47. // Changelog:
  48. // ----------
  49. // 2019.11.01 [1.1] Add GM storage for objs, getCSVheader, cumulative timers
  50. // Add implements() and make checkProperty() private
  51. // Name change, backward compatible
  52. // 2019.10.27 [1.0] First version
  53. // 2019.10.26 [0.1] First test version, private use only
  54. //
  55. // --------------------------------------------------------------------
  56.  
  57. /* jshint esversion: 6, laxbreak: true, -W008, supernew: true */
  58. /* exported UU, Library_Version_USERSCRIPT_UTILS */
  59.  
  60. const Library_Version_USERSCRIPT_UTILS = '1.0';
  61.  
  62. /* How to use this library
  63.  
  64. This library instantitates an UU object with utility variables and methods:
  65.  
  66. - me: script name as returned by GM_info
  67.  
  68. - isUndef(p): check if p is undefined
  69.  
  70. - implements(object, interfaceDef):
  71. check if passed object "implements" given interface, by checking name and
  72. type of its properties. Arguments:
  73. - object: the object to be tested
  74. - interfaceDef: array of properties to be checked, each represented by
  75. an object with:
  76. - name [mandatory]: the name of the property to be checked
  77. - type [mandatory]: the type of the property, as returned by typeof
  78. - optional: boolean, if true the property is optional (if not specified
  79. it is assumed to be false)
  80. Return true/false, and log error for each missing/mismatched property.
  81.  
  82. Logging:
  83. - le(...args): like console.error, prepending the script name
  84. - lw(...args): like console.warn, prepending the script name
  85. - li(...args): like console.info, prepending the script name
  86. - ld(...args): like console.debug, prepending the script name
  87.  
  88. Storage for objects:
  89. - GM_setObject(name, value): wrapper around GM_setValue for storing objects,
  90. applies serialization before saving.
  91. - GM_getObject(name, defaultValue): wrapper around GM_getValue for retrieving
  92. stringified objects, applies deserialization and returns a proper object.
  93. - GM_deleteObject(name): just another name for GM_deleteValue (offered only
  94. for name consistency).
  95.  
  96. CSV:
  97. - parseCSV(csv): simple CSV parsing function, by Trevor Dixon (see below)
  98. Take a CSV string as input and return an array of rows, each containing
  99. an array of fields.
  100. NOTE: it is not strict in RFC 4180 compliance as it handles unquoted
  101. double quotes inside a field (this is not allowed in the RFC specifications).
  102. - getCSVheader(csvData): return a header object from a parsed CSV.
  103. The header works as an index: there is a property for each CSV field, with
  104. the array index of that field as value.
  105. E.g.: { 'name': 0, 'date': 1, 'value': 2 } means that the CSV has two fields,
  106. the first is "name", the second is "date", the third is "value".
  107.  
  108. Promise-wrapped setTimeout:
  109. - wait(waitTime, result)
  110. return a Promise to wait for "waitTime" ms, then resolve with value "result"
  111. - thenWait(waitTime)
  112. like wait(), to be used inside a Promise.then(). Passes through the
  113. received fulfillment value.
  114.  
  115. Promise-wrapped GM_xmlhttpRequest:
  116. - GM_xhR(method, url, purpose, opts): GM_xmlhttpRequest wrapped in a Promise.
  117. Return a Promise resolving with the GM_xmlhttpRequest response, or failing
  118. with an error message (which is also logged). Arguments:
  119. - mathod: HTTP method (GET, POST, ...)
  120. - url: URL to call
  121. - purpose: string describing XHR call (for error logging and reporting)
  122. - opts: details to be passed to GM_xmlhttpRequest; the following properties
  123. will be ignored:
  124. - method, url: overwritten by function arguments
  125. - onload: overwritten to resolve the Promise
  126. - onabort, onerror, ontimeout: overwritten to reject the Promise
  127. if no context is specified, purpose is passed as context
  128.  
  129. Cumulative timers:
  130. these timers can be started and stopped multiple times, their time always
  131. adding up (unless reset):
  132. - startTimer(timer, force): create/start a timer with given "timer" name. If
  133. timer is already running, log error and do nothing if "force" is false (the
  134. default), or cancel the timer restart it if "force" is true.
  135. - stopTimer(timer): stop running timer with given "timer" name
  136. - cancelTimer(timer): stop a timer without recording time from last start
  137. - resetTimer(timer): reset a timer to zero, stopping it if needed
  138. - getTimer(timer): get time for a timer. Work if either running or stopped
  139. */
  140.  
  141.  
  142. window.UU = new (function() {
  143. 'use strict';
  144. var self = this;
  145.  
  146.  
  147.  
  148. // the name of the running script
  149. this.me = GM_info.script.name;
  150.  
  151.  
  152.  
  153. // check if argument is undefined
  154. this.isUndef = function(p) {
  155. return (typeof p === 'undefined');
  156. };
  157.  
  158.  
  159.  
  160. // Check if object "object" has property "property" of type "type".
  161. // If property is "optional" (default false), it is only checked for type
  162. // Used to test if object "implements" a specific interface
  163. function checkProperty(object, property, type, optional = false) {
  164.  
  165. if (self.isUndef(object[property])) {
  166. if (optional) return true;
  167.  
  168. self.le('Invalid object: missing property "' + property + '" of type "' + type + '"');
  169. return false;
  170. }
  171. if (typeof object[property] !== type) {
  172. self.le('Invalid object: ' + (optional ? 'optional ' : '') + 'property "' + property + '" must be of type "' + type + '"');
  173. return false;
  174. }
  175. return true;
  176. }
  177.  
  178. // check if passed object "implements" given interface, by checking name
  179. // and type of its properties.
  180. this.implements = function(object, interfaceDef) {
  181. var valid = true;
  182. try {
  183. // check is not stopped at first error, so all problems are logged
  184. interfaceDef.forEach(function(prop) {
  185. valid = valid && checkProperty(object, prop.name, prop.type, prop.optional);
  186. });
  187. } catch(err) {
  188. self.le('Error while testing object:', object,
  189. 'for interface:', interfaceDef, 'Error:', err);
  190. }
  191. return valid;
  192. };
  193.  
  194.  
  195.  
  196. // logging
  197. var bracketMe = '[' + this.me + ']';
  198. this.le = function(...args) { console.error(bracketMe, ...args); };
  199. this.lw = function(...args) { console.warn (bracketMe, ...args); };
  200. this.li = function(...args) { console.info (bracketMe, ...args); };
  201. this.ld = function(...args) { console.debug(bracketMe, ...args); };
  202.  
  203.  
  204.  
  205. // storage for objects
  206. // setter...
  207. this.GM_setObject = function(name, value) {
  208. var jsonData;
  209. try {
  210. jsonData = JSON.stringify(value);
  211. GM_setValue(name, jsonData);
  212. } catch(err) {
  213. self.le('Error serializing object to save in storage. Name:', name, '- Object:', value, '- Error:', err);
  214. }
  215. };
  216.  
  217. // ...and getter
  218. this.GM_getObject = function(name, defaultValue) {
  219. var jsonData = GM_getValue(name);
  220. if (jsonData) {
  221. try {
  222. return JSON.parse(jsonData);
  223. } catch(err) {
  224. self.le('Error parsing object retrieved from storage. Name:', name, '- Object:', jsonData, '- Error:', err);
  225. }
  226. } else return defaultValue;
  227. };
  228.  
  229. // deleteObject offered only for name consistency
  230. this.GM_deleteObject = GM_deleteValue;
  231.  
  232.  
  233.  
  234. // Simple, compact and fast CSV parsing function, by Trevor Dixon:
  235. // https://stackoverflow.com/a/14991797
  236. // take a CSV as input and return an array of arrays (rows, fields)
  237. /* eslint-disable max-statements, max-statements-per-line, max-len */
  238. this.parseCSV = function(csv) {
  239. var arr = [];
  240. var quote = false; // true means we're inside a quoted field
  241.  
  242. // iterate over each character, keep track of current row and column (of the returned array)
  243. var row, col, c;
  244. for (row = col = c = 0; c < csv.length; c++) {
  245. var cc = csv[c], nc = csv[c+1]; // current character, next character
  246. arr[row] = arr[row] || []; // create a new row if necessary
  247. arr[row][col] = arr[row][col] || ''; // create a new column (start with empty string) if necessary
  248.  
  249. // If the current character is a quotation mark, and we're inside a
  250. // quoted field, and the next character is also a quotation mark,
  251. // add a quotation mark to the current column and skip the next character
  252. if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
  253.  
  254. // If it's just one quotation mark, begin/end quoted field
  255. if (cc == '"') { quote = !quote; continue; }
  256.  
  257. // If it's a comma and we're not in a quoted field, move on to the next column
  258. if (cc == ',' && !quote) { ++col; continue; }
  259.  
  260. // If it's a newline (CRLF) and we're not in a quoted field, skip the next character
  261. // and move on to the next row and move to column 0 of that new row
  262. if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }
  263.  
  264. // If it's a newline (LF or CR) and we're not in a quoted field,
  265. // move on to the next row and move to column 0 of that new row
  266. if (cc == '\n' && !quote) { ++row; col = 0; continue; }
  267. if (cc == '\r' && !quote) { ++row; col = 0; continue; }
  268.  
  269. // Otherwise, append the current character to the current column
  270. arr[row][col] += cc;
  271. }
  272. return arr;
  273. };
  274. /* eslint-enable max-statements, max-statements-per-line, max-len */
  275.  
  276. // return a header object from a parsed CSV
  277. this.getCSVheader = function(csvData) {
  278. var header = csvData[0], fields = {};
  279. for (var i = 0; i < header.length; i++) fields[header[i]] = i;
  280. return fields;
  281. };
  282.  
  283.  
  284.  
  285. // setTimeout wrapped in a Promise
  286. this.wait = function(waitTime, result) {
  287. return new Promise(function(resolve, _I_reject) {
  288. setTimeout(resolve, waitTime, result);
  289. });
  290. };
  291.  
  292. // setTimeout wrapped in a Promise, if called iside "then"
  293. this.thenWait = function(waitTime) {
  294. return (function(result) { return self.wait(waitTime, result); });
  295. };
  296.  
  297.  
  298.  
  299. // handle download error in a Promise-enhanced GM_xmlhttpRequest
  300. function xhrError(rejectFunc, response, method, url, purpose, reason) {
  301. var m = purpose + ' - HTTP ' + method + ' error' + (reason ? ' (' + reason + ')' : '') + ': '
  302. + response.status + (response.statusText ? ' - ' + response.statusText : '');
  303. self.le(m, 'URL: ' + url, 'Response:', response);
  304. rejectFunc(m);
  305. }
  306. function xhrErrorFunc(rejectFunc, method, url, purpose, reason) {
  307. return (function(resp) {
  308. xhrError(rejectFunc, resp, method, url, purpose, reason);
  309. });
  310. }
  311.  
  312.  
  313. // wrap GM_xmlhttpRequest in a Promise
  314. // returns a Promise resolving with the GM_xmlhttpRequest response
  315. this.GM_xhR = function(method, url, purpose, opts) {
  316. return new Promise(function(resolve, reject) {
  317. var details = opts || {};
  318. details.method = method;
  319. details.url = url;
  320. details.onload = function(response) {
  321. if (response.status !== 200) xhrError(reject, response, method, url, purpose);
  322. // else resolve(response);
  323. else {
  324. if (details.responseType === 'document') {
  325. try {
  326. const d = document.implementation.createHTMLDocument().documentElement;
  327. d.innerHTML = response.responseText;
  328. response.responseXML2 = d;
  329. } catch(e) {
  330. xhrError(reject, response, method, url, purpose, e);
  331. }
  332. }
  333. resolve(response);
  334. }
  335. };
  336. details.onabort = xhrErrorFunc(reject, method, url, purpose, 'abort');
  337. details.onerror = xhrErrorFunc(reject, method, url, purpose, 'error');
  338. details.ontimeout = xhrErrorFunc(reject, method, url, purpose, 'timeout');
  339. if (self.isUndef(details.synchronous)) details.synchronous = false;
  340. if (self.isUndef(details.context)) details.context = purpose;
  341. GM_xmlhttpRequest(details);
  342. });
  343. };
  344.  
  345.  
  346.  
  347. // cumulative timers
  348. var timers = {};
  349. // create/start a timer
  350. this.startTimer = function(timer, force = false) {
  351. timers[timer] = timers[timer] || { 'time': 0, 'start': null };
  352. if (timers[timer].start !== null) {
  353. if (force) self.cancelTimer(timer);
  354. else {
  355. self.le('Timer already running:', timer);
  356. return;
  357. }
  358. }
  359. timers[timer].start = performance.now();
  360. };
  361.  
  362. // stop a running timer
  363. this.stopTimer = function(timer) {
  364. var stop = performance.now();
  365. if (!timers[timer] || timers[timer].start === null) {
  366. self.le('No running timer specified with name', timer);
  367. return;
  368. }
  369. timers[timer].time += stop - timers[timer].start;
  370. timers[timer].start = null;
  371. return timers[timer].time;
  372. };
  373.  
  374. // stop a timer without recording time
  375. this.cancelTimer = function(timer) {
  376. if (!timers[timer]) {
  377. self.le('No timer specified with name', timer);
  378. return;
  379. }
  380. timers[timer].start = null;
  381. };
  382.  
  383. // reset a timer to zero, stopping it if needed
  384. this.resetTimer = function(timer) {
  385. if (!timers[timer]) {
  386. self.le('No timer specified with name', timer);
  387. return;
  388. }
  389. timers[timer] = { 'time': 0, 'start': null };
  390. };
  391.  
  392. // get time for a (possibly running) timer
  393. this.getTimer = function(timer) {
  394. var now = performance.now();
  395. if (!timers[timer]) {
  396. self.le('No timer specified with name', timer);
  397. return;
  398. }
  399. return timers[timer].time
  400. + (timers[timer].start != null ? now - timers[timer].start : 0);
  401. };
  402.  
  403.  
  404.  
  405. })();