SingleFile - 单文件保存网页

将当前网页保存为一个.html网页文件

  1. /* eslint-disable no-multi-spaces */
  2. /* eslint-disable no-useless-call */
  3.  
  4. // ==UserScript==
  5. // @name SingleFile - 单文件保存网页
  6. // @name:en SingleFile - Webpage downloader
  7. // @name:en-US SingleFile - Webpage downloader
  8. // @name:en-UK SingleFile - Webpage downloader
  9. // @name:zh SingleFile - 单文件保存网页
  10. // @name:zh-CN SingleFile - 单文件保存网页
  11. // @name:zh-Hans SingleFile - 单文件保存网页
  12. // @name:zh-TW SingleFile - 單檔案保存網頁
  13. // @namespace SingleFile
  14. // @version 2.2
  15. // @description 将当前网页保存为一个.html网页文件
  16. // @description:en Save webpages into one .html file
  17. // @description:en-US Save webpages into one .html file
  18. // @description:en-UK Save webpages into one .html file
  19. // @description:zh 将当前网页保存为一个.html网页文件
  20. // @description:zh-CN 将当前网页保存为一个.html网页文件
  21. // @description:zh-Hans 将当前网页保存为一个.html网页文件
  22. // @description:zh-TW 將當前網頁保存為一個.html網頁檔案
  23. // @author PY-DNG
  24. // @license MIT
  25. // @include *
  26. // @connect *
  27. // @icon data:image/png;base64,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
  28. // @grant GM_xmlhttpRequest
  29. // @grant GM_registerMenuCommand
  30. // @grant GM_unregisterMenuCommand
  31. // @grant GM_info
  32. // @noframes
  33. // ==/UserScript==
  34.  
  35. (function() {
  36. 'use strict';
  37.  
  38. // Arguments: level=LogLevel.Info, logContent, asObject=false
  39. // Needs one call "DoLog();" to get it initialized before using it!
  40. function DoLog() {
  41. // Global log levels set
  42. window.LogLevel = {
  43. None: 0,
  44. Error: 1,
  45. Success: 2,
  46. Warning: 3,
  47. Info: 4,
  48. }
  49. window.LogLevelMap = {};
  50. window.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'}
  51. window.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'}
  52. window.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'}
  53. window.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'}
  54. window.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'}
  55. window.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}
  56.  
  57. // Current log level
  58. DoLog.logLevel = (unsafeWindow ? unsafeWindow.isPY_DNG : window.isPY_DNG) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  59.  
  60. // Log counter
  61. DoLog.logCount === undefined && (DoLog.logCount = 0);
  62. if (++DoLog.logCount > 512) {
  63. console.clear();
  64. DoLog.logCount = 0;
  65. }
  66.  
  67. // Get args
  68. let level, logContent, asObject;
  69. switch (arguments.length) {
  70. case 1:
  71. level = LogLevel.Info;
  72. logContent = arguments[0];
  73. asObject = false;
  74. break;
  75. case 2:
  76. level = arguments[0];
  77. logContent = arguments[1];
  78. asObject = false;
  79. break;
  80. case 3:
  81. level = arguments[0];
  82. logContent = arguments[1];
  83. asObject = arguments[2];
  84. break;
  85. default:
  86. level = LogLevel.Info;
  87. logContent = 'DoLog initialized.';
  88. asObject = false;
  89. break;
  90. }
  91.  
  92. // Log when log level permits
  93. if (level <= DoLog.logLevel) {
  94. let msg = '%c' + LogLevelMap[level].prefix;
  95. let subst = LogLevelMap[level].color;
  96.  
  97. if (asObject) {
  98. msg += ' %o';
  99. } else {
  100. switch(typeof(logContent)) {
  101. case 'string': msg += ' %s'; break;
  102. case 'number': msg += ' %d'; break;
  103. case 'object': msg += ' %o'; break;
  104. }
  105. }
  106.  
  107. console.log(msg, subst, logContent);
  108. }
  109. }
  110. DoLog();
  111.  
  112. bypassXB();
  113. GM_PolyFill('default');
  114.  
  115. // Inner consts with i18n
  116. const CONST = {
  117. Number: {
  118. Max_XHR: 20,
  119. MaxUrlLength: 4096
  120. },
  121. Text: {
  122. 'zh-CN': {
  123. SavePage: '保存此网页',
  124. Saving: '保存中{A}',
  125. About: '<!-- Web Page Saved By {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
  126. .replaceAll('{SCNM}', GM_info.script.name)
  127. .replaceAll('{VRSN}', GM_info.script.version)
  128. .replaceAll('{ATNM}', GM_info.script.author)
  129. .replaceAll('{LINK}', location.href)
  130. },
  131. 'zh-Hans': {
  132. SavePage: '保存此网页',
  133. Saving: '保存中{A}',
  134. About: '<!-- Web Page Saved By {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
  135. .replaceAll('{SCNM}', GM_info.script.name)
  136. .replaceAll('{VRSN}', GM_info.script.version)
  137. .replaceAll('{ATNM}', GM_info.script.author)
  138. .replaceAll('{LINK}', location.href)
  139. },
  140. 'zh': {
  141. SavePage: '保存此网页',
  142. Saving: '保存中{A}',
  143. About: '<!-- Web Page Saved By {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
  144. .replaceAll('{SCNM}', GM_info.script.name)
  145. .replaceAll('{VRSN}', GM_info.script.version)
  146. .replaceAll('{ATNM}', GM_info.script.author)
  147. .replaceAll('{LINK}', location.href)
  148. },
  149. 'zh-TW': {
  150. SavePage: '保存此網頁',
  151. Saving: '保存中{A}',
  152. About: '<!-- Web Page Saved By {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
  153. .replaceAll('{SCNM}', GM_info.script.name)
  154. .replaceAll('{VRSN}', GM_info.script.version)
  155. .replaceAll('{ATNM}', GM_info.script.author)
  156. .replaceAll('{LINK}', location.href)
  157. },
  158. 'en-US': {
  159. SavePage: 'Save this webpage',
  160. Saving: 'Saving, please wait{A}',
  161. About: '<!-- Web Page Saved By {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
  162. .replaceAll('{SCNM}', GM_info.script.name)
  163. .replaceAll('{VRSN}', GM_info.script.version)
  164. .replaceAll('{ATNM}', GM_info.script.author)
  165. .replaceAll('{LINK}', location.href)
  166. },
  167. 'en-UK': {
  168. SavePage: 'Save this webpage',
  169. Saving: 'Saving, please wait{A}',
  170. About: '<!-- Web Page Saved By {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
  171. .replaceAll('{SCNM}', GM_info.script.name)
  172. .replaceAll('{VRSN}', GM_info.script.version)
  173. .replaceAll('{ATNM}', GM_info.script.author)
  174. .replaceAll('{LINK}', location.href)
  175. },
  176. 'en': {
  177. SavePage: 'Save this webpage',
  178. Saving: 'Saving, please wait{A}',
  179. About: '<!-- Web Page Saved By {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
  180. .replaceAll('{SCNM}', GM_info.script.name)
  181. .replaceAll('{VRSN}', GM_info.script.version)
  182. .replaceAll('{ATNM}', GM_info.script.author)
  183. .replaceAll('{LINK}', location.href)
  184. },
  185. 'default': {
  186. SavePage: 'Save this webpage',
  187. Saving: 'Saving, please wait{A}',
  188. About: '<!-- Web Page Saved By {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
  189. .replaceAll('{SCNM}', GM_info.script.name)
  190. .replaceAll('{VRSN}', GM_info.script.version)
  191. .replaceAll('{ATNM}', GM_info.script.author)
  192. .replaceAll('{LINK}', location.href)
  193. }
  194. }
  195. }
  196.  
  197. // Get i18n code
  198. let i18n = navigator.language;
  199. if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}
  200.  
  201. // XHRHOOK
  202. GMXHRHook(CONST.Number.Max_XHR);
  203.  
  204. main()
  205. function main() {
  206. // GUI
  207. let button = GM_registerMenuCommand(CONST.Text[i18n].SavePage, onclick);
  208. const SAnime = new SavingAnime;
  209. SAnime.model = CONST.Text[i18n].Saving;
  210. SAnime.callback = function(text) {
  211. GM_unregisterMenuCommand(button);
  212. button = GM_registerMenuCommand(text, () => {});
  213. }
  214.  
  215. function onclick() {
  216. SAnime.start();
  217. Generate_Single_File({
  218. onfinish: (FinalHTML) => {
  219. saveTextToFile(FinalHTML, 'SingleFile - {Title} - {Time}.html'.replace('{Title}', document.title).replace('{Time}', getTime('-', '-')));
  220. GM_unregisterMenuCommand(button);
  221. SAnime.stop();
  222. button = GM_registerMenuCommand(CONST.Text[i18n].SavePage, onclick);
  223. }
  224. });
  225. }
  226.  
  227. function SavingAnime() {
  228. const SA = this;
  229. SA.model = '{A}';
  230. SA.time = 1000;
  231. SA.index = 0;
  232. SA.frames = ['... ', ' ... ', ' ...', '. ..', '.. .'];
  233. SA.callback = (frametext) => {console.log(frametext);};
  234.  
  235. SA.nextframe = function() {
  236. SA.index++;
  237. SA.index > SA.frames.length-1 && (SA.index = 0);
  238. SA.callback(SA.model.replace('{A}', SA.frames[SA.index]));
  239. return true;
  240. };
  241.  
  242. SA.start = function() {
  243. if (SA.interval) {return false;}
  244. SA.index = 0;
  245. SA.interval = setInterval(SA.nextframe, SA.time);
  246. return true;
  247. }
  248.  
  249. SA.stop = function() {
  250. if (!SA.interval) {return false;}
  251. clearInterval(SA.interval);
  252. SA.interval = 0;
  253. return true;
  254. }
  255. };
  256. }
  257.  
  258. function Generate_Single_File(details) {
  259. // Init DOM
  260. const html = document.querySelector('html').outerHTML;
  261. const dom = (new DOMParser()).parseFromString(html, 'text/html');
  262.  
  263. // Functions
  264. const _J = (args) => {const a = []; for (let i = 0; i < args.length; i++) {a.push(args[i]);}; return a;};
  265. const $ = function() {return dom.querySelector.apply(dom, _J(arguments))};
  266. const $_ = function() {return dom.querySelectorAll.apply(dom, _J(arguments))};
  267. const $C = function() {return dom.createElement.apply(dom, _J(arguments))};
  268. const $A = (a,b) => (a.appendChild(b));
  269. const $I = (a,b) => (b.parentElement ? b.parentElement.insertBefore(a, b) : null);
  270. const $R = (e) => (e.parentElement ? e.parentElement.removeChild(e) : null);
  271. const ishttp = (s) => (!/^[^\/:]*:/.test(s) || /^https?:\/\//.test(s));
  272. const ElmProps = new (function() {
  273. const props = this.props = {};
  274. const cssMap = this.cssMap = new Map();
  275.  
  276. this.getCssPath = function(elm) {
  277. return cssMap.get(elm) || (cssMap.set(elm, cssPath(elm)), cssMap.get(elm));
  278. }
  279.  
  280. this.add = function(elm, type, value) {
  281. const path = cssPath(elm);
  282. const EPList = props[path] = props[path] || [];
  283. const EProp = {};
  284. EProp.type = type;
  285. EProp.value = value;
  286. EPList.push(EProp);
  287. }
  288. });
  289.  
  290. // Hook GM_xmlhttpRequest
  291. const AM = new AsyncManager();
  292. AM.onfinish = function() {
  293. // Add applyProps script
  294. const script = $C('script');
  295. script.innerText = "window.addEventListener('load', function(){({FUNC})({PROPS});})"
  296. .replace('{PROPS}', JSON.stringify(ElmProps.props))
  297. .replace('{FUNC}', `function(c){const funcs={Canvas:{DataUrl:function(a,b){const img=new Image();const ctx=a.getContext('2d');img.onload=()=>{ctx.drawImage(img,0,0)};img.src=b}},Input:{Value:function(a,b){a.value=b}}};for(const[cssPath,propList]of Object.entries(c)){const elm=document.querySelector(cssPath);for(const prop of propList){const type=prop.type;const value=prop.value;const funcPath=type.split('.');let func=funcs;for(let i=0;i<funcPath.length;i++){func=func[funcPath[i]]}func(elm,value)}}}`);
  298. $A(dom.head, script);
  299.  
  300. // Generate html
  301. const FinalHTML = '{ABOUT}\n\n{HTML}'.replace('{ABOUT}', CONST.Text[i18n].About).replace('{HTML}', dom.querySelector('html').outerHTML)
  302.  
  303. DoLog(LogLevel.Success, 'Single File Generation Complete.')
  304. DoLog([dom, FinalHTML]);
  305. details.onfinish(FinalHTML)
  306. };
  307.  
  308. // Change document.characterSet to utf8
  309. DoLog('SingleFile: Setting charset');
  310. if (document.characterSet !== 'UTF-8') {
  311. const meta = $('meta[http-equiv="Content-Type"][content*="charset"]');
  312. meta && (meta.content = meta.content.replace(/charset\s*=\s*[^;\s]*/i, 'charset=UTF-8'));
  313. }
  314.  
  315. // Clear scripts
  316. DoLog('SingleFile: Clearing scripts');
  317. for (const script of $_('script')) {
  318. $R(script);
  319. }
  320.  
  321. // Clear inline-scripts
  322. DoLog('SingleFile: Clearing inline scripts');
  323. for (const elm of $_('*')) {
  324. const ISKeys = ['onabort', 'onerror', 'onresize', 'onscroll', 'onunload', 'oncancel', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'onclose', 'oncuechange', 'ondblclick', 'ondrag', 'ondragend', 'ondragenter', 'ondragexit', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onfocus', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onreset', 'onresize', 'onscroll', 'onseeked', 'onseeking', 'onselect', 'onshow', 'onstalled', 'onsubmit', 'onsuspend', 'ontimeupdate', 'ontoggle', 'onvolumechange', 'onwaiting', 'onbegin', 'onend', 'onrepeat'];
  325. for (const key of ISKeys) {
  326. elm.removeAttribute(key);
  327. elm[key] = undefined;
  328. }
  329. }
  330.  
  331. // Clear preload-scripts
  332. DoLog('SingleFile: Clearing preload scripts');
  333. for (const link of $_('link[rel*=modulepreload]')) {
  334. $R(link);
  335. }
  336.  
  337. // Remove "Content-Security-Policy" meta header
  338. DoLog('SingleFile: Removing "Content-Security-Policy" meta headers');
  339. for (const m of $_('meta[http-equiv="Content-Security-Policy"]')) {
  340. $R(m);
  341. }
  342.  
  343. // Deal styles
  344. /*
  345. DoLog('SingleFile: Dealing linked stylesheets');
  346. for (const link of $_('link[rel="stylesheet"]')) {
  347. if (!link.href) {continue;}
  348. const href = link.href;
  349. AM.add();
  350. requestText(href, (t, l) => {
  351. const s = $C('style');
  352. s.innerText = t;
  353. $I(s, l);
  354. $R(l);
  355. AM.finish();
  356. }, link);
  357. }
  358. */
  359.  
  360. // Deal Style url(http) links
  361. DoLog('SingleFile: Dealing style urls');
  362. for (const link of $_('link[rel*=stylesheet][href]')) {
  363. dealLinkedStyle(link)
  364. }
  365. for (const elm of $_('style')) {
  366. elm.innerText && dealStyle(elm.innerText, (style, elm) => (elm.innerHTML = style), elm);
  367. }
  368.  
  369. // Deal <link>s
  370. DoLog('SingleFile: Dealing links');
  371. for (const link of $_('link[href]')) {
  372. // Only deal http[s] links
  373. if (!link.href) {continue;}
  374. if (!ishttp(link.href)) {continue;}
  375.  
  376. // Only deal links that rel includes one of the following:
  377. // icon, apple-touch-icon, apple-touch-startup-image, prefetch, preload, prerender, manifest, stylesheet
  378. // And in the same time NOT includes any of the following:
  379. // alternate
  380. let deal = false;
  381. const accepts = ['icon', 'apple-touch-icon', 'apple-touch-startup-image', 'prefetch', 'preload', 'prerender', 'manifest', 'stylesheet'];
  382. const excludes = ['alternate']
  383. const rels = link.rel.split(' ');
  384. for (const rel of rels) {
  385. deal = deal || (accepts.includes(rel) && !excludes.includes(rel));
  386. }
  387. if (!deal) {continue;}
  388.  
  389. // Save original href to link.ohref
  390. link.ohref = link.href;
  391.  
  392. AM.add();
  393. requestDataURL(link.href, function(durl, link) {
  394. link.href = durl;
  395.  
  396. // Deal style if links to a stylesheet
  397. if (rels.includes('stylesheet')) {
  398. dealLinkedStyle(link);
  399. }
  400. AM.finish();
  401. }, link);
  402. }
  403.  
  404. // Deal images' and sources' src
  405. DoLog('SingleFile: Dealing images\' & sources\' src');
  406. for (const img of $_('img[src], source[src]')) {
  407. // Get full src
  408. if (img.src.length > CONST.Number.MaxUrlLength) {continue;}
  409. if (!img.src) {continue;}
  410. if (!ishttp(img.src)) {continue;}
  411. const src = fullurl(img.src);
  412.  
  413. // Get original img element
  414. const path = ElmProps.getCssPath(img);
  415. const oimg = document.querySelector(path);
  416.  
  417. // Get data url
  418. let url;
  419. try {
  420. if (!oimg.complete) {throw new Error();}
  421. url = img2url(oimg);
  422. img.src = url;
  423. } catch (e) {
  424. if (img.src) {
  425. AM.add();
  426. requestDataURL(src, (url) => {
  427. img.src = url;
  428. AM.finish();
  429. });
  430. }
  431. }
  432. }
  433.  
  434. // Deal images' and sources' srcset
  435. DoLog('SingleFile: Dealing images\' & sources\' srcset');
  436. for (const img of $_('img[srcset], source[srcset]')) {
  437. // Check if empty
  438. if (!img.srcset) {continue;}
  439.  
  440. // Get all srcs list
  441. const list = img.srcset.split(',');
  442. for (let i = 0; i < list.length; i++) {
  443. const srcitem = list[i].trim();
  444. if (srcitem.length > CONST.Number.MaxUrlLength) {continue;}
  445. if (!srcitem) {continue}
  446. const parts = srcitem.replaceAll(/(\s){2,}/g, '$1').split(' ');
  447. if (!ishttp(parts[0])) {continue};
  448. const src = fullurl(parts[0]);
  449.  
  450. list[i] = {
  451. src: src,
  452. rest: parts.slice(1, parts.length).join(' '),
  453. parts: parts,
  454. dataurl: null,
  455. string: null
  456. };
  457. }
  458.  
  459. // Get all data urls into list
  460. const S_AM = new AsyncManager();
  461. const dlist = [];
  462. S_AM.onfinish = function() {
  463. img.srcset = dlist.join(',');
  464. AM.finish();
  465. }
  466. AM.add();
  467. for (const srcobj of list) {
  468. S_AM.add();
  469. requestDataURL(srcobj.src, (url, srcobj) => {
  470. srcobj.dataurl = url;
  471. srcobj.string = [srcobj.dataurl, srcobj.rest].join(' ');
  472. dlist.push(srcobj.string);
  473. S_AM.finish();
  474. }, srcobj);
  475. }
  476. S_AM.finishEvent = true;
  477. }
  478.  
  479. // Deal canvases
  480. DoLog('SingleFile: Dealing canvases');
  481. for (const cvs of $_('canvas')) {
  482. let url;
  483. try {
  484. url = img2url(cvs);
  485. ElmProps.add(cvs, 'Canvas.DataUrl', url);
  486. } catch (e) {}
  487. }
  488.  
  489. // Deal background-images
  490. DoLog('SingleFile: Dealing background-images');
  491. for (const elm of $_('*')) {
  492. const urlReg = /^\s*url\(\s*['"]?([^\(\)'"]+)['"]?\s*\)\s*$/;
  493. const bgImage = elm.style.backgroundImage;
  494. if (!bgImage) {continue;}
  495. if (bgImage.length > CONST.Number.MaxUrlLength) {continue;}
  496. if (bgImage === 'url("https://images.weserv.nl/?url=https://ae01.alicdn.com/kf/H3bbe45ee0a3841ec9644e1ea9aa157742.jpg")') {debugger;}
  497. if (bgImage && urlReg.test(bgImage)) {
  498. // Get full image url
  499. let url = bgImage.match(urlReg)[1];
  500. if (/^data:/.test(url)) {continue;}
  501. url = fullurl(url);
  502.  
  503. // Get image
  504. AM.add();
  505. requestDataURL(url, function(durl, elm) {
  506. elm.style.backgroundImage = 'url({U})'.replace('{U}', durl);
  507. AM.finish();
  508. }, elm);
  509. }
  510. }
  511.  
  512. // Deal input/textarea/progress values
  513. DoLog('SingleFile: Dealing values');
  514. for (const elm of $_('input,textarea,progress')) {
  515. // Query origin element's value
  516. const cssPath = ElmProps.getCssPath(elm);
  517. const oelm = document.querySelector(cssPath);
  518.  
  519. // Add to property map
  520. oelm.value && ElmProps.add(elm, 'Input.Value', oelm.value);
  521. }
  522.  
  523. // Get favicon.ico if no icon found
  524. DoLog('SingleFile: Dealing favicon.ico');
  525. if (!$('link[rel*=icon]')) {
  526. const I_AM = new AsyncManager();
  527. GM_xmlhttpRequest({
  528. method: 'GET',
  529. url: getHost() + 'favicon.ico',
  530. responseType: 'blob',
  531. onload: (e) => {
  532. if (e.status >= 200 && e.status < 300) {
  533. blobToDataURL(e.response, (durl) => {
  534. const icon = $C('link');
  535. icon.rel = 'icon';
  536. icon.href = durl;
  537. $A(dom.head, icon);
  538. });
  539. }
  540. I_AM.finish();
  541. }
  542. })
  543. }
  544.  
  545. // Start generating the finish event
  546. DoLog('SingleFile: Waiting for async tasks to be finished');
  547. AM.finishEvent = true;
  548.  
  549. function dealStyle(style, callback, args=[]) {
  550. const re = /url\(\s*['"]?([^\(\)'"]+)['"]?\s*\)/;
  551. const rg = /url\(\s*['"]?([^\(\)'"]+)['"]?\s*\)/g;
  552. const replace = (durl, urlexp, arg1, arg2, arg3) => {
  553. // Replace style text
  554. const durlexp = 'url("{D}")'.replace('{D}', durl);
  555. style = style.replaceAll(urlexp, durlexp);
  556.  
  557. // Get args
  558. const args = [style];
  559. for (let i = 2; i < arguments.length; i++) {
  560. args.push(arguments[i]);
  561. }
  562. callback.apply(null, args);
  563. AM.finish();
  564. };
  565.  
  566. const all = style.match(rg);
  567. if (!all) {return;}
  568. for (const urlexp of all) {
  569. // Check url
  570. if (urlexp.length > CONST.Number.MaxUrlLength) {continue;}
  571. const osrc = urlexp.match(re)[1];
  572. const baseurl = args instanceof HTMLLinkElement && args.ohref ? args.ohref : location.href;
  573. if (!ishttp(osrc)) {continue;}
  574. const src = fullurl(osrc, baseurl);
  575.  
  576. // Request
  577. AM.add();
  578. requestDataURL(src, replace, [urlexp].concat(args));
  579. }
  580. }
  581. function dealLinkedStyle(link) {
  582. if (!link.href || !/^data:/.test(link.href)) {return;}
  583. const durl = link.href;
  584. const blob = dataURLToBlob(durl);
  585. const reader = new FileReader();
  586. reader.onload = () => {
  587. dealStyle(reader.result, (style, link) => {
  588. const blob = new Blob([style],{type:"text/css"});
  589. AM.add();
  590. blobToDataURL(blob, function(durl, link) {
  591. link.href = durl;
  592. AM.finish();
  593. }, link)
  594. }, link);
  595. AM.finish();
  596. }
  597. AM.add();
  598. reader.readAsText(blob);
  599. }
  600. }
  601.  
  602. // This function is expected to be used on output html
  603. function applyProps(props) {
  604. const funcs = {
  605. Canvas: {
  606. DataUrl: function(elm, value) {
  607. const img = new Image();
  608. const ctx = elm.getContext('2d');
  609. img.onload = () => {ctx.drawImage(img, 0, 0);};
  610. img.src = value;
  611. }
  612. },
  613. Input: {
  614. Value: function(elm, value) {
  615. elm.value = value;
  616. }
  617. }
  618. };
  619.  
  620. for (const [cssPath, propList] of Object.entries(props)) {
  621. const elm = document.querySelector(cssPath);
  622. for (const prop of propList) {
  623. const type = prop.type;
  624. const value = prop.value;
  625.  
  626. // Get function
  627. const funcPath = type.split('.');
  628. let func = funcs;
  629. for (let i = 0; i < funcPath.length; i++) {
  630. func = func[funcPath[i]];
  631. }
  632.  
  633. // Call function
  634. func(elm, value);
  635. }
  636. }
  637. }
  638.  
  639. function fullurl(url, baseurl=location.href) {
  640. if (/^\/{2,}/.test(url)) {url = location.protocol + url;}
  641. if (!/^https?:\/\//.test(url)) {
  642. const base = baseurl.replace(/(.+\/).*?$/, '$1');;
  643. const a = document.createElement('a');
  644. a.href = base + url;
  645. url = a.href;
  646. }
  647. return url;
  648. }
  649.  
  650. function cssPath(el) {
  651. if (!(el instanceof Element)) return;
  652. var path = [];
  653. while (el.nodeType === Node.ELEMENT_NODE) {
  654. var selector = el.nodeName.toLowerCase();
  655. if (el.id) {
  656. selector += '#' + el.id;
  657. path.unshift(selector);
  658. break;
  659. } else {
  660. var sib = el,
  661. nth = 1;
  662. while (sib = sib.previousElementSibling) {
  663. if (sib.nodeName.toLowerCase() == selector) nth++;
  664. }
  665. if (nth != 1) selector += ":nth-of-type(" + nth + ")";
  666. }
  667. path.unshift(selector);
  668. el = el.parentNode;
  669. }
  670. return path.join(" > ");
  671. }
  672.  
  673. function requestText(url, callback, args=[]) {
  674. GM_xmlhttpRequest({
  675. method: 'GET',
  676. url: url,
  677. responseType: 'text',
  678. onload: function(response) {
  679. const text = response.responseText;
  680. const argvs = [text].concat(args);
  681. callback.apply(null, argvs);
  682. }
  683. })
  684. }
  685.  
  686. function requestDataURL(url, callback, args=[]) {
  687. GM_xmlhttpRequest({
  688. method: 'GET',
  689. url: url,
  690. responseType: 'blob',
  691. onload: function(response) {
  692. const blob = response.response;
  693. blobToDataURL(blob, function(url) {
  694. const argvs = [url].concat(args);
  695. callback.apply(null, argvs);
  696. })
  697. }
  698. })
  699. }
  700.  
  701. function blobToDataURL(blob, callback, args=[]) {
  702. const reader = new FileReader();
  703. reader.onload = function () {
  704. callback.apply(null, [reader.result].concat(args));
  705. }
  706. reader.readAsDataURL(blob);
  707. }
  708.  
  709. function dataURLToBlob(dataurl) {
  710. let arr = dataurl.split(','),
  711. mime = arr[0].match(/:(.*?);/)[1],
  712. bstr = atob(arr[1]),
  713. n = bstr.length,
  714. u8arr = new Uint8Array(n)
  715. while (n--) {
  716. u8arr[n] = bstr.charCodeAt(n)
  717. }
  718. return new Blob([u8arr], { type: mime })
  719. }
  720.  
  721. function XHRFinisher() {
  722. const XHRF = this;
  723.  
  724. // Ongoing xhr count
  725. this.xhrCount = 0;
  726.  
  727. // Whether generate finish events
  728. this.finishEvent = false;
  729.  
  730. // Original xhr
  731. this.GM_xmlhttpRequest = GM_xmlhttpRequest;
  732.  
  733. // xhr provided for outer scope
  734. GM_xmlhttpRequest = function(details) {
  735. DoLog('XHRFinisher: Requesting ' + details.url);
  736.  
  737. // Hook functions that will be called when xhr stops
  738. details.onload = wrap(details.onload)
  739. details.ontimeout = wrap(details.ontimeout)
  740. details.onerror = wrap(details.onerror)
  741. details.onabort = wrap(details.onabort)
  742.  
  743. // Count increase
  744. XHRF.xhrCount++;
  745.  
  746. // Start xhr
  747. XHRF.GM_xmlhttpRequest(details);
  748.  
  749. function wrap(ofunc) {
  750. return function(e) {
  751. DoLog('XHRFinisher: Request ' + details.url + ' finish. ' + (XHRF.xhrCount-1).toString() + ' requests rest. ');
  752. ofunc(e);
  753. --XHRF.xhrCount === 0 && XHRF.finishEvent && XHRF.onfinish && XHRF.onfinish();
  754. }
  755. }
  756. }
  757. }
  758.  
  759. function AsyncManager() {
  760. const AM = this;
  761.  
  762. // Ongoing xhr count
  763. this.taskCount = 0;
  764.  
  765. // Whether generate finish events
  766. let finishEvent = false;
  767. Object.defineProperty(this, 'finishEvent', {
  768. configurable: true,
  769. enumerable: true,
  770. get: () => (finishEvent),
  771. set: (b) => {
  772. finishEvent = b;
  773. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  774. }
  775. });
  776.  
  777. // Add one task
  778. this.add = () => (++AM.taskCount);
  779.  
  780. // Finish one task
  781. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  782. }
  783.  
  784. function img2url(img) {
  785. const cvs = document.createElement('canvas');
  786. const ctx = cvs.getContext('2d');
  787. cvs.width = img.width;
  788. cvs.height = img.height;
  789. ctx.drawImage(img, 0, 0)
  790. return cvs.toDataURL();
  791. }
  792.  
  793. // Get a time text like 1970-01-01 00:00:00
  794. // if dateSpliter provided false, there will be no date part. The same for timeSpliter.
  795. function getTime(dateSpliter='-', timeSpliter=':') {
  796. const d = new Date();
  797. let fulltime = ''
  798. fulltime += dateSpliter ? fillNumber(d.getFullYear(), 4) + dateSpliter + fillNumber((d.getMonth() + 1), 2) + dateSpliter + fillNumber(d.getDate(), 2) : '';
  799. fulltime += dateSpliter && timeSpliter ? ' ' : '';
  800. fulltime += timeSpliter ? fillNumber(d.getHours(), 2) + timeSpliter + fillNumber(d.getMinutes(), 2) + timeSpliter + fillNumber(d.getSeconds(), 2) : '';
  801. return fulltime;
  802. }
  803.  
  804. // Just stopPropagation and preventDefault
  805. function destroyEvent(e) {
  806. if (!e) {return false;};
  807. if (!e instanceof Event) {return false;};
  808. e.stopPropagation();
  809. e.preventDefault();
  810. }
  811.  
  812. // GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
  813. // Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
  814. // (If the request is invalid, such as url === '', will return false and will NOT make this request)
  815. // If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
  816. // Requires: function delItem(){...} & function uniqueIDMaker(){...}
  817. function GMXHRHook(maxXHR=5) {
  818. const GM_XHR = GM_xmlhttpRequest;
  819. const getID = uniqueIDMaker();
  820. let todoList = [], ongoingList = [];
  821. GM_xmlhttpRequest = safeGMxhr;
  822.  
  823. function safeGMxhr() {
  824. // Get an id for this request, arrange a request object for it.
  825. const id = getID();
  826. const request = {id: id, args: arguments, aborter: null};
  827.  
  828. // Deal onload function first
  829. dealEndingEvents(request);
  830.  
  831. /* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
  832. // Stop invalid requests
  833. if (!validCheck(request)) {
  834. return false;
  835. }
  836. */
  837.  
  838. // Judge if we could start the request now or later?
  839. todoList.push(request);
  840. checkXHR();
  841. return makeAbortFunc(id);
  842.  
  843. // Decrease activeXHRCount while GM_XHR onload;
  844. function dealEndingEvents(request) {
  845. const e = request.args[0];
  846.  
  847. // onload event
  848. const oriOnload = e.onload;
  849. e.onload = function() {
  850. reqFinish(request.id);
  851. checkXHR();
  852. oriOnload ? oriOnload.apply(null, arguments) : function() {};
  853. }
  854.  
  855. // onerror event
  856. const oriOnerror = e.onerror;
  857. e.onerror = function() {
  858. reqFinish(request.id);
  859. checkXHR();
  860. oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
  861. }
  862.  
  863. // ontimeout event
  864. const oriOntimeout = e.ontimeout;
  865. e.ontimeout = function() {
  866. reqFinish(request.id);
  867. checkXHR();
  868. oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
  869. }
  870.  
  871. // onabort event
  872. const oriOnabort = e.onabort;
  873. e.onabort = function() {
  874. reqFinish(request.id);
  875. checkXHR();
  876. oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
  877. }
  878. }
  879.  
  880. // Check if the request is invalid
  881. function validCheck(request) {
  882. const e = request.args[0];
  883.  
  884. if (!e.url) {
  885. return false;
  886. }
  887.  
  888. return true;
  889. }
  890.  
  891. // Call a XHR from todoList and push the request object to ongoingList if called
  892. function checkXHR() {
  893. if (ongoingList.length >= maxXHR) {return false;};
  894. if (todoList.length === 0) {return false;};
  895. const req = todoList.shift();
  896. const reqArgs = req.args;
  897. const aborter = GM_XHR.apply(null, reqArgs);
  898. req.aborter = aborter;
  899. ongoingList.push(req);
  900. return req;
  901. }
  902.  
  903. // Make a function that aborts a certain request
  904. function makeAbortFunc(id) {
  905. return function() {
  906. let i;
  907.  
  908. // Check if the request haven't been called
  909. for (i = 0; i < todoList.length; i++) {
  910. const req = todoList[i];
  911. if (req.id === id) {
  912. // found this request: haven't been called
  913. delItem(todoList, i);
  914. return true;
  915. }
  916. }
  917.  
  918. // Check if the request is running now
  919. for (i = 0; i < ongoingList.length; i++) {
  920. const req = todoList[i];
  921. if (req.id === id) {
  922. // found this request: running now
  923. req.aborter();
  924. reqFinish(id);
  925. checkXHR();
  926. }
  927. }
  928.  
  929. // Oh no, this request is already finished...
  930. return false;
  931. }
  932. }
  933.  
  934. // Remove a certain request from ongoingList
  935. function reqFinish(id) {
  936. let i;
  937. for (i = 0; i < ongoingList.length; i++) {
  938. const req = ongoingList[i];
  939. if (req.id === id) {
  940. ongoingList = delItem(ongoingList, i);
  941. return true;
  942. }
  943. }
  944. return false;
  945. }
  946. }
  947. }
  948.  
  949. function parseDocument(htmlblob, callback, args=[]) {
  950. const reader = new FileReader();
  951. reader.onload = function(e) {
  952. const htmlText = reader.result;
  953. const dom = new DOMParser().parseFromString(htmlText, 'text/html');
  954. args = [dom].concat(args);
  955. callback.apply(null, args);
  956. //callback(dom, htmlText);
  957. }
  958. reader.readAsText(htmlblob, 'GBK');
  959. }
  960.  
  961. // Get a url argument from lacation.href
  962. // also recieve a function to deal the matched string
  963. // returns defaultValue if name not found
  964. // Args: name, dealFunc=(function(a) {return a;}), defaultValue=null
  965. function getUrlArgv(details) {
  966. typeof(details) === 'string' && (details = {name: details});
  967. typeof(details) === 'undefined' && (details = {});
  968. if (!details.name) {return null;};
  969.  
  970. const url = details.url ? details.url : location.href;
  971. const name = details.name ? details.name : '';
  972. const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;});
  973. const defaultValue = details.defaultValue ? details.defaultValue : null;
  974. const matcher = new RegExp(name + '=([^&]+)');
  975. const result = url.match(matcher);
  976. const argv = result ? dealFunc(result[1]) : defaultValue;
  977.  
  978. return argv;
  979. }
  980.  
  981. // Append a style text to document(<head>) with a <style> element
  982. function addStyle(css, id) {
  983. const style = document.createElement("style");
  984. id && (style.id = id);
  985. style.textContent = css;
  986. for (const elm of document.querySelectorAll('#'+id)) {
  987. elm.parentElement && elm.parentElement.removeChild(elm);
  988. }
  989. document.head.appendChild(style);
  990. }
  991.  
  992. function saveTextToFile(text, name) {
  993. const blob = new Blob([text],{type:"text/plain;charset=utf-8"});
  994. const url = URL.createObjectURL(blob);
  995. const a = document.createElement('a');
  996. a.href = url;
  997. a.download = name;
  998. a.click();
  999. }
  1000.  
  1001. // File download function
  1002. // details looks like the detail of GM_xmlhttpRequest
  1003. // onload function will be called after file saved to disk
  1004. function downloadFile(details) {
  1005. if (!details.url || !details.name) {return false;};
  1006.  
  1007. // Configure request object
  1008. const requestObj = {
  1009. url: details.url,
  1010. responseType: 'blob',
  1011. onload: function(e) {
  1012. // Save file
  1013. saveFile(URL.createObjectURL(e.response), details.name);
  1014.  
  1015. // onload callback
  1016. details.onload ? details.onload(e) : function() {};
  1017. }
  1018. }
  1019. if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;};
  1020. if (details.onprogress ) {requestObj.onprogress = details.onprogress;};
  1021. if (details.onerror ) {requestObj.onerror = details.onerror;};
  1022. if (details.onabort ) {requestObj.onabort = details.onabort;};
  1023. if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
  1024. if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;};
  1025.  
  1026. // Send request
  1027. GM_xmlhttpRequest(requestObj);
  1028. }
  1029.  
  1030. // get '/' splited API array from a url
  1031. function getAPI(url=location.href) {
  1032. return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
  1033. }
  1034.  
  1035. // get host part from a url(includes '^https://', '/$')
  1036. function getHost(url=location.href) {
  1037. const match = location.href.match(/https?:\/\/[^\/]+\//);
  1038. return match ? match[0] : match;
  1039. }
  1040.  
  1041. // Your code here...
  1042. // Bypass xbrowser's useless GM_functions
  1043. function bypassXB() {
  1044. if (typeof(mbrowser) === 'object') {
  1045. window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined;
  1046. }
  1047. }
  1048.  
  1049. // GM_Polyfill By PY-DNG
  1050. // 2021.07.18 - 2021.07.19
  1051. // Simply provides the following GM_functions using localStorage, XMLHttpRequest and window.open:
  1052. // Returns object GM_POLYFILLED which has the following properties that shows you which GM_functions are actually polyfilled:
  1053. // GM_setValue, GM_getValue, GM_deleteValue, GM_listValues, GM_xmlhttpRequest, GM_openInTab, GM_setClipboard, unsafeWindow(object)
  1054. // All polyfilled GM_functions are accessable in window object/Global_Scope(only without Tempermonkey Sandboxing environment)
  1055. function GM_PolyFill(name='default') {
  1056. const GM_POLYFILL_KEY_STORAGE = 'GM_STORAGE_POLYFILL';
  1057. let GM_POLYFILL_storage;
  1058. const GM_POLYFILLED = {
  1059. GM_setValue: true,
  1060. GM_getValue: true,
  1061. GM_deleteValue: true,
  1062. GM_listValues: true,
  1063. GM_xmlhttpRequest: true,
  1064. GM_openInTab: true,
  1065. GM_setClipboard: true,
  1066. unsafeWindow: true,
  1067. once: false
  1068. }
  1069.  
  1070. // Ignore GM_PolyFill_Once
  1071. window.GM_POLYFILLED && window.GM_POLYFILLED.once && (window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined);
  1072.  
  1073. GM_setValue_polyfill();
  1074. GM_getValue_polyfill();
  1075. GM_deleteValue_polyfill();
  1076. GM_listValues_polyfill();
  1077. GM_xmlhttpRequest_polyfill();
  1078. GM_openInTab_polyfill();
  1079. GM_setClipboard_polyfill();
  1080. unsafeWindow_polyfill();
  1081.  
  1082. function GM_POLYFILL_getStorage() {
  1083. let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
  1084. gstorage = gstorage ? JSON.parse(gstorage) : {};
  1085. let storage = gstorage[name] ? gstorage[name] : {};
  1086. return storage;
  1087. }
  1088.  
  1089. function GM_POLYFILL_saveStorage() {
  1090. let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
  1091. gstorage = gstorage ? JSON.parse(gstorage) : {};
  1092. gstorage[name] = GM_POLYFILL_storage;
  1093. localStorage.setItem(GM_POLYFILL_KEY_STORAGE, JSON.stringify(gstorage));
  1094. }
  1095.  
  1096. // GM_setValue
  1097. function GM_setValue_polyfill() {
  1098. typeof (GM_setValue) === 'function' ? GM_POLYFILLED.GM_setValue = false: window.GM_setValue = PF_GM_setValue;;
  1099.  
  1100. function PF_GM_setValue(name, value) {
  1101. GM_POLYFILL_storage = GM_POLYFILL_getStorage();
  1102. name = String(name);
  1103. GM_POLYFILL_storage[name] = value;
  1104. GM_POLYFILL_saveStorage();
  1105. }
  1106. }
  1107.  
  1108. // GM_getValue
  1109. function GM_getValue_polyfill() {
  1110. typeof (GM_getValue) === 'function' ? GM_POLYFILLED.GM_getValue = false: window.GM_getValue = PF_GM_getValue;
  1111.  
  1112. function PF_GM_getValue(name, defaultValue) {
  1113. GM_POLYFILL_storage = GM_POLYFILL_getStorage();
  1114. name = String(name);
  1115. if (GM_POLYFILL_storage.hasOwnProperty(name)) {
  1116. return GM_POLYFILL_storage[name];
  1117. } else {
  1118. return defaultValue;
  1119. }
  1120. }
  1121. }
  1122.  
  1123. // GM_deleteValue
  1124. function GM_deleteValue_polyfill() {
  1125. typeof (GM_deleteValue) === 'function' ? GM_POLYFILLED.GM_deleteValue = false: window.GM_deleteValue = PF_GM_deleteValue;
  1126.  
  1127. function PF_GM_deleteValue(name) {
  1128. GM_POLYFILL_storage = GM_POLYFILL_getStorage();
  1129. name = String(name);
  1130. if (GM_POLYFILL_storage.hasOwnProperty(name)) {
  1131. delete GM_POLYFILL_storage[name];
  1132. GM_POLYFILL_saveStorage();
  1133. }
  1134. }
  1135. }
  1136.  
  1137. // GM_listValues
  1138. function GM_listValues_polyfill() {
  1139. typeof (GM_listValues) === 'function' ? GM_POLYFILLED.GM_listValues = false: window.GM_listValues = PF_GM_listValues;
  1140.  
  1141. function PF_GM_listValues() {
  1142. GM_POLYFILL_storage = GM_POLYFILL_getStorage();
  1143. return Object.keys(GM_POLYFILL_storage);
  1144. }
  1145. }
  1146.  
  1147. // unsafeWindow
  1148. function unsafeWindow_polyfill() {
  1149. typeof (unsafeWindow) === 'object' ? GM_POLYFILLED.unsafeWindow = false: window.unsafeWindow = window;
  1150. }
  1151.  
  1152. // GM_xmlhttpRequest
  1153. // not supported properties of details: synchronous binary nocache revalidate context fetch
  1154. // not supported properties of response(onload arguments[0]): finalUrl
  1155. // ---!IMPORTANT!--- DOES NOT SUPPORT CROSS-ORIGIN REQUESTS!!!!! ---!IMPORTANT!---
  1156. function GM_xmlhttpRequest_polyfill() {
  1157. typeof (GM_xmlhttpRequest) === 'function' ? GM_POLYFILLED.GM_xmlhttpRequest = false: window.GM_xmlhttpRequest = PF_GM_xmlhttpRequest;
  1158.  
  1159. // details.synchronous is not supported as Tempermonkey
  1160. function PF_GM_xmlhttpRequest(details) {
  1161. const xhr = new XMLHttpRequest();
  1162.  
  1163. // open request
  1164. const openArgs = [details.method, details.url, true];
  1165. if (details.user && details.password) {
  1166. openArgs.push(details.user);
  1167. openArgs.push(details.password);
  1168. }
  1169. xhr.open.apply(xhr, openArgs);
  1170.  
  1171. // set headers
  1172. if (details.headers) {
  1173. for (const key of Object.keys(details.headers)) {
  1174. xhr.setRequestHeader(key, details.headers[key]);
  1175. }
  1176. }
  1177. details.cookie ? xhr.setRequestHeader('cookie', details.cookie) : function () {};
  1178. details.anonymous ? xhr.setRequestHeader('cookie', '') : function () {};
  1179.  
  1180. // properties
  1181. xhr.timeout = details.timeout;
  1182. xhr.responseType = details.responseType;
  1183. details.overrideMimeType ? xhr.overrideMimeType(details.overrideMimeType) : function () {};
  1184.  
  1185. // events
  1186. xhr.onabort = details.onabort;
  1187. xhr.onerror = details.onerror;
  1188. xhr.onloadstart = details.onloadstart;
  1189. xhr.onprogress = details.onprogress;
  1190. xhr.onreadystatechange = details.onreadystatechange;
  1191. xhr.ontimeout = details.ontimeout;
  1192. xhr.onload = function (e) {
  1193. const response = {
  1194. readyState: xhr.readyState,
  1195. status: xhr.status,
  1196. statusText: xhr.statusText,
  1197. responseHeaders: xhr.getAllResponseHeaders(),
  1198. response: xhr.response
  1199. };
  1200. (details.responseType === '' || details.responseType === 'text') ? (response.responseText = xhr.responseText) : function () {};
  1201. (details.responseType === '' || details.responseType === 'document') ? (response.responseXML = xhr.responseXML) : function () {};
  1202. details.onload(response);
  1203. }
  1204.  
  1205. // send request
  1206. details.data ? xhr.send(details.data) : xhr.send();
  1207.  
  1208. return {
  1209. abort: xhr.abort
  1210. };
  1211. }
  1212. }
  1213.  
  1214. // NOTE: options(arg2) is NOT SUPPORTED! if provided, then will just be skipped.
  1215. function GM_openInTab_polyfill() {
  1216. typeof (GM_openInTab) === 'function' ? GM_POLYFILLED.GM_openInTab = false: window.GM_openInTab = PF_GM_openInTab;
  1217.  
  1218. function PF_GM_openInTab(url) {
  1219. window.open(url);
  1220. }
  1221. }
  1222.  
  1223. // NOTE: needs to be called in an event handler function, and info(arg2) is NOT SUPPORTED!
  1224. function GM_setClipboard_polyfill() {
  1225. typeof (GM_setClipboard) === 'function' ? GM_POLYFILLED.GM_setClipboard = false: window.GM_setClipboard = PF_GM_setClipboard;
  1226.  
  1227. function PF_GM_setClipboard(text) {
  1228. // Create a new textarea for copying
  1229. const newInput = document.createElement('textarea');
  1230. document.body.appendChild(newInput);
  1231. newInput.value = text;
  1232. newInput.select();
  1233. document.execCommand('copy');
  1234. document.body.removeChild(newInput);
  1235. }
  1236. }
  1237.  
  1238. return GM_POLYFILLED;
  1239. }
  1240.  
  1241. // Makes a function that returns a unique ID number each time
  1242. function uniqueIDMaker() {
  1243. let id = 0;
  1244. return makeID;
  1245. function makeID() {
  1246. id++;
  1247. return id;
  1248. }
  1249. }
  1250.  
  1251. // Fill number text to certain length with '0'
  1252. function fillNumber(number, length) {
  1253. let str = String(number);
  1254. for (let i = str.length; i < length; i++) {
  1255. str = '0' + str;
  1256. }
  1257. return str;
  1258. }
  1259.  
  1260. // Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
  1261. function delItem(arr, delIndex) {
  1262. arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1));
  1263. return arr;
  1264. }
  1265. })();