您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
将当前网页保存为一个纯文本的.html网页文件,不保存二进制
// ==UserScript== // @name SingleFile Pure - Pure html downloader // @name:zh SingleFile Pure - 保存纯HTML // @namespace https://gist.github.com/KnIfER // @version 3 // @description 将当前网页保存为一个纯文本的.html网页文件,不保存二进制 // @description:en Save webpages into one pure html file, without binary data. // @author PY-DNG // @license MIT // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant unsafeWindow // @icon data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE5MCIgaGVpZ2h0PSIxOTAiPgo8c3R5bGU+LnB7ZmlsbDojMjY4NGZjfTwvc3R5bGU+CjxwYXRoIGNsYXNzPSJwIiBkPSJtMTU5IDUydjEzMWwtMTMwIDB2LTE3Nmw4NiAwbS00My41IDU1LjVjMCAxMC4yLjEgMTAuNSAyIDEwIDEtMCA1LjYtMSA5LjUtMiA0LTEgOC0xLjUgOS0xLjUgMiAwIDIgMSAyIDM2LjV2MzYuNWgtMjF2MTBoNTN2LTEwbC0yMC41LS41LS41LTgzLjVjLTEyIDAtMjAgMS0yNSAyLjR6Ii8+Cjwvc3ZnPg== // @match *://*/* // @include * // @noframes // ==/UserScript== // based on @PY-DNG https://gf.zukizuki.org/zh-CN/scripts/419798-singlefile-单文件保存网页 (function() { 'use strict'; function debug(...args) { console.log("%c SingleFile: ", "color:#333!important;background:#0FF;", ...args); } var win = window.unsafeWindow || window, doc=document, d=doc , bank=win._sfpr_bank; var rM=debug, rMd=debug, err=console.error; if(!bank) { bank = win._sfpr_bank = {}; rM = GM_registerMenuCommand; rMd = GM_unregisterMenuCommand; } else try{ bank.unreg(); } catch(e){debug(e)} bank.unreg = uninstall;var unregs = []; function uninstall() { // hot-reload for(var i=0;i<unregs.length;i++) { unregs[i](); } return 1; } function addEvent(a, b, c, d) { if(!d) d = win; ((a, b, c, d)=>{ d.addEventListener(a, b, c); unregs.push(function(){ d.removeEventListener(a, b, c)} ); })(a, b, c, d); } const MSG = { 'zh': { SavePage: '保存纯网页', Saving: '保存中……' }, 'en': { SavePage: 'Save pure webpage', Saving: 'Saving, please wait……' }, } var btn, evtSt="single-file-on-before-capture-request", evtEd=evtSt.replace('before', 'after'); addEvent(evtSt, (e) => { btn = doc.getElementById('sf-pure'); if(btn) btn.remove(); }); addEvent(evtEd, (e) => { if(btn) { doc.body.append(btn); btn = 0; } }); var t = navigator.language; if(t.includes('-')) t = t.slice(0, t.indexOf('-')); // GUI var GT=MSG[t]||MSG['en'], fnMenu, menu = rM(GT.SavePage, fnMenu = function() { Generate_Single_File({ onfinish: (FinalHTML) => { var title = doc.title; saveTextToFile(FinalHTML, '{Title}.html'.replace('{Title}', title).replace('{Time}', getTime('-', '-'))); dispatchEvent(new CustomEvent(evtEd)); rMd(menu); menu = rM(GT.SavePage, fnMenu); } }); }); addEvent("single-file-pure-save", (e) => { fnMenu(); stop(e); }); if(!win.saveAsTaken) addEvent('keydown', (e) => { if(e.key=='s' && e.altKey && e.ctrlKey) { fnMenu(); stop(e); } }); function Generate_Single_File(details) { debug('Generate started...'); if(!bank.init) { dispatchEvent(new CustomEvent("single-file-user-script-init")); bank.init = 1; } dispatchEvent(new CustomEvent(evtSt)); // Init DOM var html, tmp, dom = doc; // Functions var _J = (args) => {const a = []; for (let i = 0; i < args.length; i++) {a.push(args[i]);}; return a;}; var $ = function() {return dom.querySelector.apply(dom, _J(arguments))}; var $_ = function() {return dom.querySelectorAll.apply(dom, _J(arguments))}; var $C = function() {return dom.createElement.apply(dom, _J(arguments))}; var $A = (a,b) => (a.appendChild(b)); var $R = (e) => (e.parentElement ? e.parentElement.removeChild(e) : null); function ishttp(s) { // !/^[^\/:]*:/.test(s) if(s) return s.startsWith('/') || s.startsWith('http') } dom = doc.cloneNode(1); 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) { var path = cssPath(elm), store=props[path]; if(!store) store = props[path] = []; store.push({type:type, value:value}); } }); // Generate info button! function about() { var t=$C('A'); t.id = 'sf-pure'; t.style = 'position:fixed;right:16px;top:16px;width:24px;height:24px;color:#2d2d2d;background-color:#737373;border:2px solid;border-color:#eee;border-radius:16px;z-index:2147483647;opacity:0.7;display:flex;justify-content:center;align-items:center;'; t.innerHTML = '<svg style=\'width:65%;height:65%;margin-left:1px;\' xmlns="http://www.w3.org/2000/svg"viewBox="0 0 64 64"width="64"height="64"><style>.p{fill:#f0f0f0}</style><path class="p"d="M30 3A3 3 0 1130 21 3 3 0 1130 3ZM16 25 23 29 23 58 16 63 46 63 39 58 39 25Z"/></svg>'; t.name = Date.now()+''; t.title = doc.title; t.href = location.ohref || location.href; t.target = 'blank'; return t; } const AM = new AsyncManager(); AM.onfinish = function() { // Add applyProps script var script = $C('script'); script.innerHTML = "window.addEventListener('load', function(){"+ // show info button "setTimeout(function(){var btn=document.getElementById('sf-pure');btn.title=new Date(parseInt(btn.name))+'\\n\\n'+btn.title;location.ohref=btn.href;btn.oncontextmenu=function(e){btn.style.display='none';e.preventDefault()}}, 800);" + // {FUNC} "(function(c){var fs={Canvas_DataUrl:function(a,b){var e=new Image(),v=a.getContext('2d');e.onload=function(){v.drawImage(e,0,0)};e.src=b},Input_Value:function(a,b){a.value=b}};for(var i=0,arr=Object.entries(c),t,el;i<arr.length;i++){try{t=arr[i];if(el=document.querySelector(t[0]))for(var p of t[1])fs[p.type](el,p.value)}catch(e){console.error(e)}}}" +")("+ // {PROPS} JSON.stringify(ElmProps.props) +")})"; $A(dom.head, script); $A(dom.body, about()); // Generate html var FinalHTML = dom.querySelector('html').outerHTML; debug('Generation Complete.', FinalHTML.length) details.onfinish(FinalHTML) }; // debug('Setting charset'); if (doc.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')); } // debug('strip scripts'); for (var tmp of $_('script')) { $R(tmp); } // debug('strip inline scripts'); for (var tmp of $_('*')) { var 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 (var key of ISKeys) { tmp.removeAttribute(key); tmp[key] = undefined; } } // debug('strip preload scripts'); for (var tmp of $_('link[rel*=modulepreload]')) { $R(tmp); } // debug('strip meta headers'); for (var tmp of $_('meta[http-equiv="Content-Security-Policy"]')) { $R(tmp); } //debug('Resolve style urls'); for (var tmp of $_('link[rel*=stylesheet][href]')) { resolveStyleLinked(tmp) } for (var elm of $_('style')) { //debug('style elm=', elm.id, elm) resolveStyle(elm.innerText, (style, elm) => (elm.innerHTML = style), elm); } //debug('Resolve links'); for (const link of $_('link[href]')) { // Only for http[s] links if (!link.href) {continue;} if (!ishttp(link.href)) {continue;} // Only for 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 var 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')) { resolveStyleLinked(link); } AM.finish(); }, link); } var arr = dom.links; for(var i=0;tmp=arr[i++];) { tmp.href = fullUrl(tmp.href); } //debug('Resolve image src'); for (var img of $_('img[src], source[src]')) { // Get full src // if (img.src.length > 3999) {continue;} if (!img.src) {continue;} if (!ishttp(img.src)) {continue;} img.src = fullUrl(img.src); } //debug('Resolve image srcset'); for (var img of $_('img[srcset], source[srcset]')) { if (img.srcset) { var list = img.srcset.split(','); for (let i = 0; i < list.length; i++) { // Get all srcs list var srcitem = list[i].trim(); if (srcitem.length > 3999) {continue;} if (!srcitem) {continue} var parts = srcitem.replaceAll(/(\s){2,}/g, '$1').split(' '); if (!ishttp(parts[0])) {continue}; var src = fullUrl(parts[0]); list[i] = { src: src, rest: parts.slice(1, parts.length).join(' '), parts: parts, dataurl: null, string: null }; } img.srcset = list.join(','); } } //debug('Resolve canvas'); for (var tmp of $_('canvas')) { try { var url = img2url(tmp); ElmProps.add(tmp, 'Canvas_DataUrl', url); } catch (e) {} } debug('Resolve styles', dom); for (var tmp of $_('style')) { try { if(!tmp.firstChild) { var oelm = doc.querySelector(ElmProps.getCssPath(tmp)); debug('cssRulesX', tmp, oelm); if(oelm && oelm.sheet?.cssRules?.length) { var cssRules = oelm.sheet.cssRules; var text = ''; for (var i = 0; i < cssRules.length; i++) { var rule = cssRules[i]; text += rule.cssText; text += '\n'; } //debug('cssRules', text); tmp.innerHTML = text; } } } catch (e) {} } //debug('Resolve background-images'); var urlReg = /^\s*url\(\s*['"]?([^\(\)'"]+)['"]?\s*\)\s*$/; for (var elm of $_('*')) { var url = elm.style.backgroundImage; if(url && url.length < 3999 // CONST.Number.MaxUrlLength && url.lastIndexOf('data:', 10)==-1) { // not /^data:/.test(url) url = url.match(urlReg); if (url) { // Get full image url url = fullUrl(url[1]); elm.style.backgroundImage = 'url('+url+')'; } } } //debug('Resolve input/textarea/progress values'); for (var tmp of $_('input,textarea,progress')) { // Query origin element's value var oelm = doc.querySelector(ElmProps.getCssPath(tmp)); // Add to property map oelm.value && ElmProps.add(tmp, 'Input_Value', oelm.value); } // Get favicon.ico if no icon found debug('Resolve favicon.ico'); if (!$('link[rel*=icon]')) { var icon = $C('link'); icon.rel = 'icon'; icon.href = getHost() + 'favicon.ico', $A(dom.head, icon); } // Start generating the finish event debug('Waiting for async tasks to be finished'); AM.finishEvent = true; function resolveStyle(style, callback, args=[]) { const argvs = [style].concat(args); if(!style) { return callback.apply(null, argvs); } const re = /url\(\s*['"]?([^\(\)'"]+)['"]?\s*\)/; const rg = /url\(\s*['"]?([^\(\)'"]+)['"]?\s*\)/g; const replace = (durl, urlexp, arg1, arg2, arg3) => { // Replace style text const durlexp = 'url("'+durl+'")'; style = style.replaceAll(urlexp, durlexp); // Get args argvs[0]=style; callback.apply(null, argvs); AM.finish(); }; const all = style.match(rg); if (!all) {return;} for (const urlexp of all) { // Check url if (urlexp.length > 3999) {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 resolveStyleLinked(link) { const durl = link.href; if ((durl||'')[0]!=='d') {return;} // not /^data:/.test() const blob = dataURLToBlob(durl); const reader = new FileReader(); reader.onload = () => { resolveStyle(reader.result, (style, link) => { const blob = new Blob([style],{type:"text/css"}); AM.add(); blobToDataURL(blob, function(durl, link) { //debug('style elm=', link.id, link) link.href = durl; AM.finish(); }, link) }, link); AM.finish(); } AM.add(); reader.readAsText(blob); } } var t0 = doc.createElement('a'); function fullUrl(url, baseurl) { if(url) { if (url.startsWith('//')) {url = location.protocol + url;} if (!url.startsWith('http')) { var base = (baseurl||location.href).replace(/(.+\/).*?$/, '$1'); t0.href = base + url; url = t0.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 requestDataURL(url, callback, args=[]) { try{ //debug('requestDataURL::', url, args); const argvs = [url].concat(args); callback.apply(null, argvs); }catch(e){err(e)} } function blobToDataURL(blob, callback, args=[]) { const reader = new FileReader(); reader.onload = function () { callback.apply(null, [reader.result].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 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) { var cvs = doc.createElement('canvas'); var v = cvs.getContext('2d'); cvs.width = img.width; cvs.height = img.height; v.drawImage(img, 0, 0) return cvs.toDataURL(); } // Format timecode like 1970-01-01 00:00:00 // if data-sep provided false, there will be no data part. function getTime(dateSep='-', timeSep=':') { var d = new Date(), fulltime = '' fulltime += dateSep ? f0(d.getFullYear(), 4) + dateSep + f0((d.getMonth() + 1), 2) + dateSep + f0(d.getDate(), 2) : ''; fulltime += dateSep && timeSep ? ' ' : ''; fulltime += timeSep ? f0(d.getHours(), 2) + timeSep + f0(d.getMinutes(), 2) + timeSep + f0(d.getSeconds(), 2) : ''; return fulltime; } function f0(number, ln) { var str = String(number); for (var i = str.length; i < ln; i++) { str = '0' + str; } return str; } function stop(e) { try{ e.stopPropagation(); e.preventDefault(); } catch(e) {debug(e)} } function saveTextToFile(text, name) { const blob = new Blob([text],{type:"text/plain;charset=utf-8"}); const url = URL.createObjectURL(blob); const a = doc.createElement('a'); a.href = url; a.download = name; a.click(); } // get host part from a url(includes '^https://', '/$') function getHost(url=location.href) { const match = location.href.match(/https?:\/\/[^\/]+\//); return match ? match[0] : match; } })();