Greasy Fork is available in English.
将当前网页保存为一个.html网页文件
/* eslint-disable no-multi-spaces */ /* eslint-disable no-useless-call */ // ==UserScript== // @name SingleFile - 单文件保存网页 // @name:en SingleFile - Webpage downloader // @name:en-US SingleFile - Webpage downloader // @name:en-UK SingleFile - Webpage downloader // @name:zh SingleFile - 单文件保存网页 // @name:zh-CN SingleFile - 单文件保存网页 // @name:zh-Hans SingleFile - 单文件保存网页 // @name:zh-TW SingleFile - 單檔案保存網頁 // @namespace SingleFile // @version 2.2 // @description 将当前网页保存为一个.html网页文件 // @description:en Save webpages into one .html file // @description:en-US Save webpages into one .html file // @description:en-UK Save webpages into one .html file // @description:zh 将当前网页保存为一个.html网页文件 // @description:zh-CN 将当前网页保存为一个.html网页文件 // @description:zh-Hans 将当前网页保存为一个.html网页文件 // @description:zh-TW 將當前網頁保存為一個.html網頁檔案 // @author PY-DNG // @license MIT // @include * // @connect * // @icon data:image/png;base64,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 // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_info // @noframes // ==/UserScript== (function() { 'use strict'; // Arguments: level=LogLevel.Info, logContent, asObject=false // Needs one call "DoLog();" to get it initialized before using it! function DoLog() { // Global log levels set window.LogLevel = { None: 0, Error: 1, Success: 2, Warning: 3, Info: 4, } window.LogLevelMap = {}; window.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'} window.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'} window.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'} window.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'} window.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'} window.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'} // Current log level DoLog.logLevel = (unsafeWindow ? unsafeWindow.isPY_DNG : window.isPY_DNG) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error // Log counter DoLog.logCount === undefined && (DoLog.logCount = 0); if (++DoLog.logCount > 512) { console.clear(); DoLog.logCount = 0; } // Get args let level, logContent, asObject; switch (arguments.length) { case 1: level = LogLevel.Info; logContent = arguments[0]; asObject = false; break; case 2: level = arguments[0]; logContent = arguments[1]; asObject = false; break; case 3: level = arguments[0]; logContent = arguments[1]; asObject = arguments[2]; break; default: level = LogLevel.Info; logContent = 'DoLog initialized.'; asObject = false; break; } // Log when log level permits if (level <= DoLog.logLevel) { let msg = '%c' + LogLevelMap[level].prefix; let subst = LogLevelMap[level].color; if (asObject) { msg += ' %o'; } else { switch(typeof(logContent)) { case 'string': msg += ' %s'; break; case 'number': msg += ' %d'; break; case 'object': msg += ' %o'; break; } } console.log(msg, subst, logContent); } } DoLog(); bypassXB(); GM_PolyFill('default'); // Inner consts with i18n const CONST = { Number: { Max_XHR: 20, MaxUrlLength: 4096 }, Text: { 'zh-CN': { SavePage: '保存此网页', Saving: '保存中{A}', About: '<!-- Web Page Saved By {S###} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->' .replaceAll('{S###}', GM_info.script.name) .replaceAll('{VRSN}', GM_info.script.version) .replaceAll('{ATNM}', GM_info.script.author) .replaceAll('{LINK}', location.href) }, 'zh-Hans': { SavePage: '保存此网页', Saving: '保存中{A}', About: '<!-- Web Page Saved By {S###} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->' .replaceAll('{S###}', GM_info.script.name) .replaceAll('{VRSN}', GM_info.script.version) .replaceAll('{ATNM}', GM_info.script.author) .replaceAll('{LINK}', location.href) }, 'zh': { SavePage: '保存此网页', Saving: '保存中{A}', About: '<!-- Web Page Saved By {S###} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->' .replaceAll('{S###}', GM_info.script.name) .replaceAll('{VRSN}', GM_info.script.version) .replaceAll('{ATNM}', GM_info.script.author) .replaceAll('{LINK}', location.href) }, 'zh-TW': { SavePage: '保存此網頁', Saving: '保存中{A}', About: '<!-- Web Page Saved By {S###} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->' .replaceAll('{S###}', GM_info.script.name) .replaceAll('{VRSN}', GM_info.script.version) .replaceAll('{ATNM}', GM_info.script.author) .replaceAll('{LINK}', location.href) }, 'en-US': { SavePage: 'Save this webpage', Saving: 'Saving, please wait{A}', About: '<!-- Web Page Saved By {S###} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->' .replaceAll('{S###}', GM_info.script.name) .replaceAll('{VRSN}', GM_info.script.version) .replaceAll('{ATNM}', GM_info.script.author) .replaceAll('{LINK}', location.href) }, 'en-UK': { SavePage: 'Save this webpage', Saving: 'Saving, please wait{A}', About: '<!-- Web Page Saved By {S###} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->' .replaceAll('{S###}', GM_info.script.name) .replaceAll('{VRSN}', GM_info.script.version) .replaceAll('{ATNM}', GM_info.script.author) .replaceAll('{LINK}', location.href) }, 'en': { SavePage: 'Save this webpage', Saving: 'Saving, please wait{A}', About: '<!-- Web Page Saved By {S###} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->' .replaceAll('{S###}', GM_info.script.name) .replaceAll('{VRSN}', GM_info.script.version) .replaceAll('{ATNM}', GM_info.script.author) .replaceAll('{LINK}', location.href) }, 'default': { SavePage: 'Save this webpage', Saving: 'Saving, please wait{A}', About: '<!-- Web Page Saved By {S###} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->' .replaceAll('{S###}', GM_info.script.name) .replaceAll('{VRSN}', GM_info.script.version) .replaceAll('{ATNM}', GM_info.script.author) .replaceAll('{LINK}', location.href) } } } // Get i18n code let i18n = navigator.language; if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';} // XHRHOOK GMXHRHook(CONST.Number.Max_XHR); main() function main() { // GUI let button = GM_registerMenuCommand(CONST.Text[i18n].SavePage, onclick); const SAnime = new SavingAnime; SAnime.model = CONST.Text[i18n].Saving; SAnime.callback = function(text) { GM_unregisterMenuCommand(button); button = GM_registerMenuCommand(text, () => {}); } function onclick() { SAnime.start(); Generate_Single_File({ onfinish: (FinalHTML) => { saveTextToFile(FinalHTML, 'SingleFile - {Title} - {Time}.html'.replace('{Title}', document.title).replace('{Time}', getTime('-', '-'))); GM_unregisterMenuCommand(button); SAnime.stop(); button = GM_registerMenuCommand(CONST.Text[i18n].SavePage, onclick); } }); } function SavingAnime() { const SA = this; SA.model = '{A}'; SA.time = 1000; SA.index = 0; SA.frames = ['... ', ' ... ', ' ...', '. ..', '.. .']; SA.callback = (frametext) => {console.log(frametext);}; SA.nextframe = function() { SA.index++; SA.index > SA.frames.length-1 && (SA.index = 0); SA.callback(SA.model.replace('{A}', SA.frames[SA.index])); return true; }; SA.start = function() { if (SA.interval) {return false;} SA.index = 0; SA.interval = setInterval(SA.nextframe, SA.time); return true; } SA.stop = function() { if (!SA.interval) {return false;} clearInterval(SA.interval); SA.interval = 0; return true; } }; } function Generate_Single_File(details) { // Init DOM const html = document.querySelector('html').outerHTML; const dom = (new DOMParser()).parseFromString(html, 'text/html'); // Functions const _J = (args) => {const a = []; for (let i = 0; i < args.length; i++) {a.push(args[i]);}; return a;}; const $ = function() {return dom.querySelector.apply(dom, _J(arguments))}; const $_ = function() {return dom.querySelectorAll.apply(dom, _J(arguments))}; const $C = function() {return dom.createElement.apply(dom, _J(arguments))}; const $A = (a,b) => (a.appendChild(b)); const $I = (a,b) => (b.parentElement ? b.parentElement.insertBefore(a, b) : null); const $R = (e) => (e.parentElement ? e.parentElement.removeChild(e) : null); const ishttp = (s) => (!/^[^\/:]*:/.test(s) || /^https?:\/\//.test(s)); const ElmProps = new (function() { const props = this.props = {}; const cssMap = this.cssMap = new Map(); this.getCssPath = function(elm) { return cssMap.get(elm) || (cssMap.set(elm, cssPath(elm)), cssMap.get(elm)); } this.add = function(elm, type, value) { const path = cssPath(elm); const EPList = props[path] = props[path] || []; const EProp = {}; EProp.type = type; EProp.value = value; EPList.push(EProp); } }); // Hook GM_xmlhttpRequest const AM = new AsyncManager(); AM.onfinish = function() { // Add applyProps script const script = $C('script'); script.innerText = "window.addEventListener('load', function(){({FUNC})({PROPS});})" .replace('{PROPS}', JSON.stringify(ElmProps.props)) .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)}}}`); $A(dom.head, script); // Generate html const FinalHTML = '{ABOUT}\n\n{HTML}'.replace('{ABOUT}', CONST.Text[i18n].About).replace('{HTML}', dom.querySelector('html').outerHTML) DoLog(LogLevel.Success, 'Single File Generation Complete.') DoLog([dom, FinalHTML]); details.onfinish(FinalHTML) }; // Change document.characterSet to utf8 DoLog('SingleFile: Setting charset'); if (document.characterSet !== 'UTF-8') { const meta = $('meta[http-equiv="Content-Type"][content*="charset"]'); meta && (meta.content = meta.content.replace(/charset\s*=\s*[^;\s]*/i, 'charset=UTF-8')); } // Clear scripts DoLog('SingleFile: Clearing scripts'); for (const script of $_('script')) { $R(script); } // Clear inline-scripts DoLog('SingleFile: Clearing inline scripts'); for (const elm of $_('*')) { 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']; for (const key of ISKeys) { elm.removeAttribute(key); elm[key] = undefined; } } // Clear preload-scripts DoLog('SingleFile: Clearing preload scripts'); for (const link of $_('link[rel*=modulepreload]')) { $R(link); } // Remove "Content-Security-Policy" meta header DoLog('SingleFile: Removing "Content-Security-Policy" meta headers'); for (const m of $_('meta[http-equiv="Content-Security-Policy"]')) { $R(m); } // Deal styles /* DoLog('SingleFile: Dealing linked stylesheets'); for (const link of $_('link[rel="stylesheet"]')) { if (!link.href) {continue;} const href = link.href; AM.add(); requestText(href, (t, l) => { const s = $C('style'); s.innerText = t; $I(s, l); $R(l); AM.finish(); }, link); } */ // Deal Style url(http) links DoLog('SingleFile: Dealing style urls'); for (const link of $_('link[rel*=stylesheet][href]')) { dealLinkedStyle(link) } for (const elm of $_('style')) { elm.innerText && dealStyle(elm.innerText, (style, elm) => (elm.innerHTML = style), elm); } // Deal <link>s DoLog('SingleFile: Dealing links'); for (const link of $_('link[href]')) { // Only deal http[s] links if (!link.href) {continue;} if (!ishttp(link.href)) {continue;} // Only deal links that rel includes one of the following: // icon, apple-touch-icon, apple-touch-startup-image, prefetch, preload, prerender, manifest, stylesheet // And in the same time NOT includes any of the following: // alternate let deal = false; const accepts = ['icon', 'apple-touch-icon', 'apple-touch-startup-image', 'prefetch', 'preload', 'prerender', 'manifest', 'stylesheet']; const excludes = ['alternate'] const rels = link.rel.split(' '); for (const rel of rels) { deal = deal || (accepts.includes(rel) && !excludes.includes(rel)); } if (!deal) {continue;} // Save original href to link.ohref link.ohref = link.href; AM.add(); requestDataURL(link.href, function(durl, link) { link.href = durl; // Deal style if links to a stylesheet if (rels.includes('stylesheet')) { dealLinkedStyle(link); } AM.finish(); }, link); } // Deal images' and sources' src DoLog('SingleFile: Dealing images\' & sources\' src'); for (const img of $_('img[src], source[src]')) { // Get full src if (img.src.length > CONST.Number.MaxUrlLength) {continue;} if (!img.src) {continue;} if (!ishttp(img.src)) {continue;} const src = fullurl(img.src); // Get original img element const path = ElmProps.getCssPath(img); const oimg = document.querySelector(path); // Get data url let url; try { if (!oimg.complete) {throw new Error();} url = img2url(oimg); img.src = url; } catch (e) { if (img.src) { AM.add(); requestDataURL(src, (url) => { img.src = url; AM.finish(); }); } } } // Deal images' and sources' srcset DoLog('SingleFile: Dealing images\' & sources\' srcset'); for (const img of $_('img[srcset], source[srcset]')) { // Check if empty if (!img.srcset) {continue;} // Get all srcs list const list = img.srcset.split(','); for (let i = 0; i < list.length; i++) { const srcitem = list[i].trim(); if (srcitem.length > CONST.Number.MaxUrlLength) {continue;} if (!srcitem) {continue} const parts = srcitem.replaceAll(/(\s){2,}/g, '$1').split(' '); if (!ishttp(parts[0])) {continue}; const src = fullurl(parts[0]); list[i] = { src: src, rest: parts.slice(1, parts.length).join(' '), parts: parts, dataurl: null, string: null }; } // Get all data urls into list const S_AM = new AsyncManager(); const dlist = []; S_AM.onfinish = function() { img.srcset = dlist.join(','); AM.finish(); } AM.add(); for (const srcobj of list) { S_AM.add(); requestDataURL(srcobj.src, (url, srcobj) => { srcobj.dataurl = url; srcobj.string = [srcobj.dataurl, srcobj.rest].join(' '); dlist.push(srcobj.string); S_AM.finish(); }, srcobj); } S_AM.finishEvent = true; } // Deal canvases DoLog('SingleFile: Dealing canvases'); for (const cvs of $_('canvas')) { let url; try { url = img2url(cvs); ElmProps.add(cvs, 'Canvas.DataUrl', url); } catch (e) {} } // Deal background-images DoLog('SingleFile: Dealing background-images'); for (const elm of $_('*')) { const urlReg = /^\s*url\(\s*['"]?([^\(\)'"]+)['"]?\s*\)\s*$/; const bgImage = elm.style.backgroundImage; if (!bgImage) {continue;} if (bgImage.length > CONST.Number.MaxUrlLength) {continue;} if (bgImage === 'url("https://images.weserv.nl/?url=https://ae01.alicdn.com/kf/H3bbe45ee0a3841ec9644e1ea9aa157742.jpg")') {debugger;} if (bgImage && urlReg.test(bgImage)) { // Get full image url let url = bgImage.match(urlReg)[1]; if (/^data:/.test(url)) {continue;} url = fullurl(url); // Get image AM.add(); requestDataURL(url, function(durl, elm) { elm.style.backgroundImage = 'url({U})'.replace('{U}', durl); AM.finish(); }, elm); } } // Deal input/textarea/progress values DoLog('SingleFile: Dealing values'); for (const elm of $_('input,textarea,progress')) { // Query origin element's value const cssPath = ElmProps.getCssPath(elm); const oelm = document.querySelector(cssPath); // Add to property map oelm.value && ElmProps.add(elm, 'Input.Value', oelm.value); } // Get favicon.ico if no icon found DoLog('SingleFile: Dealing favicon.ico'); if (!$('link[rel*=icon]')) { const I_AM = new AsyncManager(); GM_xmlhttpRequest({ method: 'GET', url: getHost() + 'favicon.ico', responseType: 'blob', onload: (e) => { if (e.status >= 200 && e.status < 300) { blobToDataURL(e.response, (durl) => { const icon = $C('link'); icon.rel = 'icon'; icon.href = durl; $A(dom.head, icon); }); } I_AM.finish(); } }) } // Start generating the finish event DoLog('SingleFile: Waiting for async tasks to be finished'); AM.finishEvent = true; function dealStyle(style, callback, args=[]) { const re = /url\(\s*['"]?([^\(\)'"]+)['"]?\s*\)/; const rg = /url\(\s*['"]?([^\(\)'"]+)['"]?\s*\)/g; const replace = (durl, urlexp, arg1, arg2, arg3) => { // Replace style text const durlexp = 'url("{D}")'.replace('{D}', durl); style = style.replaceAll(urlexp, durlexp); // Get args const args = [style]; for (let i = 2; i < arguments.length; i++) { args.push(arguments[i]); } callback.apply(null, args); AM.finish(); }; const all = style.match(rg); if (!all) {return;} for (const urlexp of all) { // Check url if (urlexp.length > CONST.Number.MaxUrlLength) {continue;} const osrc = urlexp.match(re)[1]; const baseurl = args instanceof HTMLLinkElement && args.ohref ? args.ohref : location.href; if (!ishttp(osrc)) {continue;} const src = fullurl(osrc, baseurl); // Request AM.add(); requestDataURL(src, replace, [urlexp].concat(args)); } } function dealLinkedStyle(link) { if (!link.href || !/^data:/.test(link.href)) {return;} const durl = link.href; const blob = dataURLToBlob(durl); const reader = new FileReader(); reader.onload = () => { dealStyle(reader.r###lt, (style, link) => { const blob = new Blob([style],{type:"text/css"}); AM.add(); blobToDataURL(blob, function(durl, link) { link.href = durl; AM.finish(); }, link) }, link); AM.finish(); } AM.add(); reader.readAsText(blob); } } // This function is expected to be used on output html function applyProps(props) { const funcs = { Canvas: { DataUrl: function(elm, value) { const img = new Image(); const ctx = elm.getContext('2d'); img.onload = () => {ctx.drawImage(img, 0, 0);}; img.src = value; } }, Input: { Value: function(elm, value) { elm.value = value; } } }; for (const [cssPath, propList] of Object.entries(props)) { const elm = document.querySelector(cssPath); for (const prop of propList) { const type = prop.type; const value = prop.value; // Get function const funcPath = type.split('.'); let func = funcs; for (let i = 0; i < funcPath.length; i++) { func = func[funcPath[i]]; } // Call function func(elm, value); } } } function fullurl(url, baseurl=location.href) { if (/^\/{2,}/.test(url)) {url = location.protocol + url;} if (!/^https?:\/\//.test(url)) { const base = baseurl.replace(/(.+\/).*?$/, '$1');; const a = document.createElement('a'); a.href = base + url; url = a.href; } return url; } function cssPath(el) { if (!(el instanceof Element)) return; var path = []; while (el.nodeType === Node.ELEMENT_NODE) { var selector = el.nodeName.toLowerCase(); if (el.id) { selector += '#' + el.id; path.unshift(selector); break; } else { var sib = el, nth = 1; while (sib = sib.previousElementSibling) { if (sib.nodeName.toLowerCase() == selector) nth++; } if (nth != 1) selector += ":nth-of-type(" + nth + ")"; } path.unshift(selector); el = el.parentNode; } return path.join(" > "); } function requestText(url, callback, args=[]) { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'text', onload: function(response) { const text = response.responseText; const argvs = [text].concat(args); callback.apply(null, argvs); } }) } function requestDataURL(url, callback, args=[]) { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(response) { const blob = response.response; blobToDataURL(blob, function(url) { const argvs = [url].concat(args); callback.apply(null, argvs); }) } }) } function blobToDataURL(blob, callback, args=[]) { const reader = new FileReader(); reader.onload = function () { callback.apply(null, [reader.r###lt].concat(args)); } reader.readAsDataURL(blob); } function dataURLToBlob(dataurl) { let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime }) } function XHRFinisher() { const XHRF = this; // Ongoing xhr count this.xhrCount = 0; // Whether generate finish events this.finishEvent = false; // Original xhr this.GM_xmlhttpRequest = GM_xmlhttpRequest; // xhr provided for outer scope GM_xmlhttpRequest = function(details) { DoLog('XHRFinisher: Requesting ' + details.url); // Hook functions that will be called when xhr stops details.onload = wrap(details.onload) details.ontimeout = wrap(details.ontimeout) details.onerror = wrap(details.onerror) details.onabort = wrap(details.onabort) // Count increase XHRF.xhrCount++; // Start xhr XHRF.GM_xmlhttpRequest(details); function wrap(ofunc) { return function(e) { DoLog('XHRFinisher: Request ' + details.url + ' finish. ' + (XHRF.xhrCount-1).toString() + ' requests rest. '); ofunc(e); --XHRF.xhrCount === 0 && XHRF.finishEvent && XHRF.onfinish && XHRF.onfinish(); } } } } function AsyncManager() { const AM = this; // Ongoing xhr count this.taskCount = 0; // Whether generate finish events let finishEvent = false; Object.defineProperty(this, 'finishEvent', { configurable: true, enumerable: true, get: () => (finishEvent), set: (b) => { finishEvent = b; b && AM.taskCount === 0 && AM.onfinish && AM.onfinish(); } }); // Add one task this.add = () => (++AM.taskCount); // Finish one task this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount)); } function img2url(img) { const cvs = document.createElement('canvas'); const ctx = cvs.getContext('2d'); cvs.width = img.width; cvs.height = img.height; ctx.drawImage(img, 0, 0) return cvs.toDataURL(); } // Get a time text like 1970-01-01 00:00:00 // if dateSpliter provided false, there will be no date part. The same for timeSpliter. function getTime(dateSpliter='-', timeSpliter=':') { const d = new Date(); let fulltime = '' fulltime += dateSpliter ? fillNumber(d.getFullYear(), 4) + dateSpliter + fillNumber((d.getMonth() + 1), 2) + dateSpliter + fillNumber(d.getDate(), 2) : ''; fulltime += dateSpliter && timeSpliter ? ' ' : ''; fulltime += timeSpliter ? fillNumber(d.getHours(), 2) + timeSpliter + fillNumber(d.getMinutes(), 2) + timeSpliter + fillNumber(d.getSeconds(), 2) : ''; return fulltime; } // Just stopPropagation and preventDefault function destroyEvent(e) { if (!e) {return false;}; if (!e instanceof Event) {return false;}; e.stopPropagation(); e.preventDefault(); } // GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR // Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting) // (If the request is invalid, such as url === '', will return false and will NOT make this request) // If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event // Requires: function delItem(){...} & function uniqueIDMaker(){...} function GMXHRHook(maxXHR=5) { const GM_XHR = GM_xmlhttpRequest; const getID = uniqueIDMaker(); let todoList = [], ongoingList = []; GM_xmlhttpRequest = safeGMxhr; function safeGMxhr() { // Get an id for this request, arrange a request object for it. const id = getID(); const request = {id: id, args: arguments, aborter: null}; // Deal onload function first dealEndingEvents(request); /* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES! // Stop invalid requests if (!validCheck(request)) { return false; } */ // Judge if we could start the request now or later? todoList.push(request); checkXHR(); return makeAbortFunc(id); // Decrease activeXHRCount while GM_XHR onload; function dealEndingEvents(request) { const e = request.args[0]; // onload event const oriOnload = e.onload; e.onload = function() { reqFinish(request.id); checkXHR(); oriOnload ? oriOnload.apply(null, arguments) : function() {}; } // onerror event const oriOnerror = e.onerror; e.onerror = function() { reqFinish(request.id); checkXHR(); oriOnerror ? oriOnerror.apply(null, arguments) : function() {}; } // ontimeout event const oriOntimeout = e.ontimeout; e.ontimeout = function() { reqFinish(request.id); checkXHR(); oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {}; } // onabort event const oriOnabort = e.onabort; e.onabort = function() { reqFinish(request.id); checkXHR(); oriOnabort ? oriOnabort.apply(null, arguments) : function() {}; } } // Check if the request is invalid function validCheck(request) { const e = request.args[0]; if (!e.url) { return false; } return true; } // Call a XHR from todoList and push the request object to ongoingList if called function checkXHR() { if (ongoingList.length >= maxXHR) {return false;}; if (todoList.length === 0) {return false;}; const req = todoList.shift(); const reqArgs = req.args; const aborter = GM_XHR.apply(null, reqArgs); req.aborter = aborter; ongoingList.push(req); return req; } // Make a function that aborts a certain request function makeAbortFunc(id) { return function() { let i; // Check if the request haven't been called for (i = 0; i < todoList.length; i++) { const req = todoList[i]; if (req.id === id) { // found this request: haven't been called delItem(todoList, i); return true; } } // Check if the request is running now for (i = 0; i < ongoingList.length; i++) { const req = todoList[i]; if (req.id === id) { // found this request: running now req.aborter(); reqFinish(id); checkXHR(); } } // Oh no, this request is already finished... return false; } } // Remove a certain request from ongoingList function reqFinish(id) { let i; for (i = 0; i < ongoingList.length; i++) { const req = ongoingList[i]; if (req.id === id) { ongoingList = delItem(ongoingList, i); return true; } } return false; } } } function parseDocument(htmlblob, callback, args=[]) { const reader = new FileReader(); reader.onload = function(e) { const htmlText = reader.r###lt; const dom = new DOMParser().parseFromString(htmlText, 'text/html'); args = [dom].concat(args); callback.apply(null, args); //callback(dom, htmlText); } reader.readAsText(htmlblob, 'GBK'); } // Get a url argument from lacation.href // also recieve a function to deal the matched string // returns defaultValue if name not found // Args: name, dealFunc=(function(a) {return a;}), defaultValue=null function getUrlArgv(details) { typeof(details) === 'string' && (details = {name: details}); typeof(details) === 'undefined' && (details = {}); if (!details.name) {return null;}; const url = details.url ? details.url : location.href; const name = details.name ? details.name : ''; const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;}); const defaultValue = details.defaultValue ? details.defaultValue : null; const matcher = new RegExp(name + '=([^&]+)'); const r###lt = url.match(matcher); const argv = r###lt ? dealFunc(r###lt[1]) : defaultValue; return argv; } // Append a style text to document(<head>) with a <style> element function addStyle(css, id) { const style = document.createElement("style"); id && (style.id = id); style.textContent = css; for (const elm of document.querySelectorAll('#'+id)) { elm.parentElement && elm.parentElement.removeChild(elm); } document.head.appendChild(style); } function saveTextToFile(text, name) { const blob = new Blob([text],{type:"text/plain;charset=utf-8"}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; a.click(); } // File download function // details looks like the detail of GM_xmlhttpRequest // onload function will be called after file saved to disk function downloadFile(details) { if (!details.url || !details.name) {return false;}; // Configure request object const requestObj = { url: details.url, responseType: 'blob', onload: function(e) { // Save file saveFile(URL.createObjectURL(e.response), details.name); // onload callback details.onload ? details.onload(e) : function() {}; } } if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;}; if (details.onprogress ) {requestObj.onprogress = details.onprogress;}; if (details.onerror ) {requestObj.onerror = details.onerror;}; if (details.onabort ) {requestObj.onabort = details.onabort;}; if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;}; if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;}; // Send request GM_xmlhttpRequest(requestObj); } // get '/' splited API array from a url function getAPI(url=location.href) { return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g); } // get host part from a url(includes '^https://', '/$') function getHost(url=location.href) { const match = location.href.match(/https?:\/\/[^\/]+\//); return match ? match[0] : match; } // Your code here... // Bypass xbrowser's useless GM_functions function bypassXB() { if (typeof(mbrowser) === 'object') { window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined; } } // GM_Polyfill By PY-DNG // 2021.07.18 - 2021.07.19 // Simply provides the following GM_functions using localStorage, XMLHttpRequest and window.open: // Returns object GM_POLYFILLED which has the following properties that shows you which GM_functions are actually polyfilled: // GM_setValue, GM_getValue, GM_deleteValue, GM_listValues, GM_xmlhttpRequest, GM_openInTab, GM_setClipboard, unsafeWindow(object) // All polyfilled GM_functions are accessable in window object/Global_Scope(only without Tempermonkey Sandboxing environment) function GM_PolyFill(name='default') { const GM_POLYFILL_KEY_STORAGE = 'GM_STORAGE_POLYFILL'; let GM_POLYFILL_storage; const GM_POLYFILLED = { GM_setValue: true, GM_getValue: true, GM_deleteValue: true, GM_listValues: true, GM_xmlhttpRequest: true, GM_openInTab: true, GM_setClipboard: true, unsafeWindow: true, once: false } // Ignore GM_PolyFill_Once 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); GM_setValue_polyfill(); GM_getValue_polyfill(); GM_deleteValue_polyfill(); GM_listValues_polyfill(); GM_xmlhttpRequest_polyfill(); GM_openInTab_polyfill(); GM_setClipboard_polyfill(); unsafeWindow_polyfill(); function GM_POLYFILL_getStorage() { let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE); gstorage = gstorage ? JSON.parse(gstorage) : {}; let storage = gstorage[name] ? gstorage[name] : {}; return storage; } function GM_POLYFILL_saveStorage() { let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE); gstorage = gstorage ? JSON.parse(gstorage) : {}; gstorage[name] = GM_POLYFILL_storage; localStorage.setItem(GM_POLYFILL_KEY_STORAGE, JSON.stringify(gstorage)); } // GM_setValue function GM_setValue_polyfill() { typeof (GM_setValue) === 'function' ? GM_POLYFILLED.GM_setValue = false: window.GM_setValue = PF_GM_setValue;; function PF_GM_setValue(name, value) { GM_POLYFILL_storage = GM_POLYFILL_getStorage(); name = String(name); GM_POLYFILL_storage[name] = value; GM_POLYFILL_saveStorage(); } } // GM_getValue function GM_getValue_polyfill() { typeof (GM_getValue) === 'function' ? GM_POLYFILLED.GM_getValue = false: window.GM_getValue = PF_GM_getValue; function PF_GM_getValue(name, defaultValue) { GM_POLYFILL_storage = GM_POLYFILL_getStorage(); name = String(name); if (GM_POLYFILL_storage.hasOwnProperty(name)) { return GM_POLYFILL_storage[name]; } else { return defaultValue; } } } // GM_deleteValue function GM_deleteValue_polyfill() { typeof (GM_deleteValue) === 'function' ? GM_POLYFILLED.GM_deleteValue = false: window.GM_deleteValue = PF_GM_deleteValue; function PF_GM_deleteValue(name) { GM_POLYFILL_storage = GM_POLYFILL_getStorage(); name = String(name); if (GM_POLYFILL_storage.hasOwnProperty(name)) { delete GM_POLYFILL_storage[name]; GM_POLYFILL_saveStorage(); } } } // GM_listValues function GM_listValues_polyfill() { typeof (GM_listValues) === 'function' ? GM_POLYFILLED.GM_listValues = false: window.GM_listValues = PF_GM_listValues; function PF_GM_listValues() { GM_POLYFILL_storage = GM_POLYFILL_getStorage(); return Object.keys(GM_POLYFILL_storage); } } // unsafeWindow function unsafeWindow_polyfill() { typeof (unsafeWindow) === 'object' ? GM_POLYFILLED.unsafeWindow = false: window.unsafeWindow = window; } // GM_xmlhttpRequest // not supported properties of details: synchronous binary nocache revalidate context fetch // not supported properties of response(onload arguments[0]): finalUrl // ---!IMPORTANT!--- DOES NOT SUPPORT CROSS-ORIGIN REQUESTS!!!!! ---!IMPORTANT!--- function GM_xmlhttpRequest_polyfill() { typeof (GM_xmlhttpRequest) === 'function' ? GM_POLYFILLED.GM_xmlhttpRequest = false: window.GM_xmlhttpRequest = PF_GM_xmlhttpRequest; // details.synchronous is not supported as Tempermonkey function PF_GM_xmlhttpRequest(details) { const xhr = new XMLHttpRequest(); // open request const openArgs = [details.method, details.url, true]; if (details.user && details.password) { openArgs.push(details.user); openArgs.push(details.password); } xhr.open.apply(xhr, openArgs); // set headers if (details.headers) { for (const key of Object.keys(details.headers)) { xhr.setRequestHeader(key, details.headers[key]); } } details.cookie ? xhr.setRequestHeader('cookie', details.cookie) : function () {}; details.anonymous ? xhr.setRequestHeader('cookie', '') : function () {}; // properties xhr.timeout = details.timeout; xhr.responseType = details.responseType; details.overrideMimeType ? xhr.overrideMimeType(details.overrideMimeType) : function () {}; // events xhr.onabort = details.onabort; xhr.onerror = details.onerror; xhr.onloadstart = details.onloadstart; xhr.onprogress = details.onprogress; xhr.onreadystatechange = details.onreadystatechange; xhr.ontimeout = details.ontimeout; xhr.onload = function (e) { const response = { readyState: xhr.readyState, status: xhr.status, statusText: xhr.statusText, responseHeaders: xhr.getAllResponseHeaders(), response: xhr.response }; (details.responseType === '' || details.responseType === 'text') ? (response.responseText = xhr.responseText) : function () {}; (details.responseType === '' || details.responseType === 'document') ? (response.responseXML = xhr.responseXML) : function () {}; details.onload(response); } // send request details.data ? xhr.send(details.data) : xhr.send(); return { abort: xhr.abort }; } } // NOTE: options(arg2) is NOT SUPPORTED! if provided, then will just be skipped. function GM_openInTab_polyfill() { typeof (GM_openInTab) === 'function' ? GM_POLYFILLED.GM_openInTab = false: window.GM_openInTab = PF_GM_openInTab; function PF_GM_openInTab(url) { window.open(url); } } // NOTE: needs to be called in an event handler function, and info(arg2) is NOT SUPPORTED! function GM_setClipboard_polyfill() { typeof (GM_setClipboard) === 'function' ? GM_POLYFILLED.GM_setClipboard = false: window.GM_setClipboard = PF_GM_setClipboard; function PF_GM_setClipboard(text) { // Create a new textarea for copying const newInput = document.createElement('textarea'); document.body.appendChild(newInput); newInput.value = text; newInput.select(); document.execCommand('copy'); document.body.removeChild(newInput); } } return GM_POLYFILLED; } // Makes a function that returns a unique ID number each time function uniqueIDMaker() { let id = 0; return makeID; function makeID() { id++; return id; } } // Fill number text to certain length with '0' function fillNumber(number, length) { let str = String(number); for (let i = str.length; i < length; i++) { str = '0' + str; } return str; } // Del a item from an array using its index. Returns the array but can NOT modify the original array directly!! function delItem(arr, delIndex) { arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1)); return arr; } })();