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