SingleFile - 单文件保存网页

将当前网页保存为一个.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})();
SingleFile - 单文件保存网页 | Robomonkey