SingleFile - Webpage downloader

Save webpages into one .html file

As of 22.01.2022. See ბოლო ვერსია.

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