/* 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,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAYAAABS3GwHAAAAAXNSR0IArs4c6QAACSxJREFUeF7t3VFollUcx/Hn2fRGItQiuiuKtJtyXXkVjVCCiHwvQvRuXkV6MQcRiILvIEGoYCIDs4vlvTYGgaKGZUh3FnbhHBE0UCiCzcLdvHvfJ55iNKfb89/7P+855/+cr7c75zn/8z+/z85e9/Iuz/hHBxLuQJ7w3tk6HcgAQAiS7gAAkj5+Ng8AMpB0BwCQ9PGzeQCQgaQ7YApAURSNLMt2JH1ia29+Ps/zU/RH3oGoAcw9KBqXZ7IdV6Y72excZ/Mff+eHF1ryzaU4cv+r81+Ovvv0gRT33s2eowQwO1c0vrjReeOrW8XhVqebbaU7Z2b21+z4O1tBIIxAdAAu3i6GPpxsTxB84QmuGFYCKP+BQNa/qACc/aEY+vSb9oSsdEY9rgNLAEAgy0c0AMrv/MMXCL/s2FYftRwACKq7GQWA8mf+XePtyepyGVHVgZUAQLB2x6IAcPB8e+zqdDFcdbh8vboDjwMAgtX7FhzAL38WjbfP8N2/OtqyEasBAMHj+xccwPj37eap74rjsuNlVFUH1gIAgke7FxzAvolW8+bdHABVyRZ+vQoACB5uZHAAg6dbzXv3ASDMd+UwCQAQ/N/G4AAGTrbGFhZzXgBXRls2QAoABP/1MyiA8s1t20/wAlgWbdmo9QAAQXgAze0n2vz8L8u2aNR6AaSOIPQNAABRrOWDugGQMgIAyLNlYmS3AFJFAAATsZYXqQGQIgIAyLNlYqQWQGoIAGAi1vIiXQBICQEA5NkyMdIVgFQQAMBErOVFugSQAgIAyLNlYqRrAHVHAAATsZYX2QsAdUYAAHm2TIzsFYC6IgCAiVjLi+wlgDoiAIA8WyZG9hpA3RAAwESs5UX6AFAnBACQZ8vESF8A6oIAACZiLS/SJ4A6IACAPFsmRvoGYB0BAEzEWl5kCACWEQBAni0TI0MBsIoAACZiLS8yJACLCAAgz5aJkaEBWEMAABOxlhcZAwBLCAAgz5aJkbEAsIIAACZiLS8yJgAWEABAni0TI2MDEDsCAJiItbzIGAHEjAAA8myZGBkrgFgRAMBErOVFxgwgRgQAkGfLxMjYAcSGAAAmYi0v0gKAmBAAQJ4tEyOtAIgFAQBMxLq+Rd452j+S5/lYqB0CIFTnWfffDtw52j+a53kzVDsAEKrzrAuAoij4AxmJQ+AG4E8kJU0AAAAAAK8Bks5A0pvnBuAGAAA3QNIZSHrz3ADcAADgBkg6A0lvnhuAGwAA3ABJZyDpzXMDcAMAgBsg6QwkvXluAG4AAHADJJ2BpDfPDcANAABugKQzkPTmuQG4AQDADZB0BpLePDcANwAAuAGSzkDSm+cG4AYAADeAvwzMHNsQ9JMw/O20u5W2fbxYdDezu1ncAJ5vAACsHVQAdAe5q1khPhYFAABY3oGgPw4AoKvvGz2dxA3Q0/Y+/HAAeGy2cCkACBvlYhgAXHTR7TMA4Lafaz4NAB6bLVwKAMJGuRgGABdddPsMALjtJzeAx366WAoALroofAY3gLBRHocBwGOzQwDwuD2WEnSA3wR7/k2w4EwY4rEDAACAx7jFtxQAABBfKj1WBAAAeIxbfEsBAADxpdJjRQAAgMe4xbcUAAAQXyo9VgQAAHiMW3xLAQAA8aXSY0UAAIDHuMW3FAAAEF8qPVYEAAB4jFt8SwEAAPGl0mNFAPAMgE+FWDvdvB3ao/4Qb4cGAACWd4CPRfEI3sJS3AAeT4kbwGOzhUsBQNgoF8MA4KKLbp8BALf9XPNpAPDYbOFSABA2ysUwALjoottnAMBtP7kBPPbTxVIAcNFF4TO4AYSN8jgMAB6bDQCPzRYuBQBho1wMCwHARd08w10HeCuE57dCuDs6nuSiAwAAgIscmX0GAABgNrwuCgcAAFzkyOwzAAAAs+F1UTgAAOAiR2afAQAAmA2vi8IBAAAXOTL7DAAAwGx4XRQOAAC4yJHZZwAAAGbD66JwAADARY7MPgMAADAbXheFA8AzgLp9LIrvty+7CP3yZwAAAKpMAUDVvozPBdL1L/hsAOiOAAC6/gWfDQDdEQBA17/gswGgOwIA6PoXfDYAdEcAAF3/gs8GgO4IAKDrX/DZANAdAQB0/Qs+GwC6IwCArn/BZwNAdwTJAdC1i9muO8Bvgj3/Jtj1AfI8XQcAAABdgozPBgAAjEdYVz4AAKBLkPHZAACA8QjrygcAAHQJMj4bAAAwHmFd+QAAgC5BxmcDAADGI6wrHwAA0CXI+GwAAMB4hHXlAwAAugQZnw0AABiPsK58AABAlyDjswEAAOMR1pUPAADoEmR8NgAAYDzCuvIBAABdgozPBgAAjEdYVz4AAKBLkPHZAACA8QjrygcAAHQJMj4bAAAwHmFd+QAAgC5BxmcDAADGI6wrHwAA0CXI+GwAAMB4hHXlAwAAugQZnw0AABiPsK58AABAlyDjswEAAOMR1pUPAADoEmR8NgAAYDzCuvIBAABdgozPBgAAjEdYVz4AAKBLkPHZqQNobD/RnjR+hpSv6EDSAMq+DZxsjS0s5sOKHjLVaAc2bcyyHz/qH8nzfCzUFoL+mdRy04OnW8179/PjoRrAuuE68PxT2dTlDzY0wlWQZcEB7JtoNW/eBUDIEIRae/DFfPTs/v5mqPXLdYMD+PxGu/nZtYIbIGQKAq19ZHc+emBn4gBm54rGrnFeCAfKYNBlrxzqH3luS7if/6O4AcoiDp5vj12dLnghHDSOfhff80rf1Cd7+oL+/B8NAG4Bv+ELvVr5vz+XDvWPPPtE2O/+0QAoC7l4uxgavtCeCH04rN/7DpzZm597c1v/UO9Xql4h+Ivg5SWOXy+GTl0HQfWx2R1xZHd+7sDOOMIf1Q2wdKSTtxaHjn2dTbQ6dg+Zyh/twMa+LDv9Xjzf+ZcqjOoGWCqqfE0wfr0zOPkzL4ytYyqDv/e1vqn3X8+/jeFn/pX9jBLAUpFzD4rGpenOwLWZLPttvrP597/y4YWW9UjUu/7yBe4zT2ZTL2zNf3rr5SwbfKlvfsum8C92V+t61ABWFl0URfnfZgP1jpD53c2HfG/PertnCsB6N8d4OlDVAQBUdYiv17oDAKj18bK5qg4AoKpDfL3WHQBArY+XzVV1AABVHeLrte4AAGp9vGyuqgP/AG8AnxsGCe8KAAAAAElFTkSuQmCC
// @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 {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
.replaceAll('{SCNM}', 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 {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
.replaceAll('{SCNM}', 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 {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
.replaceAll('{SCNM}', 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 {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
.replaceAll('{SCNM}', 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 {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
.replaceAll('{SCNM}', 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 {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
.replaceAll('{SCNM}', 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 {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
.replaceAll('{SCNM}', 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 {SCNM} Ver.{VRSN}, Author {ATNM} -->\n<!-- Page URL: {LINK} -->'
.replaceAll('{SCNM}', 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.result, (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.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 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.result;
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 result = url.match(matcher);
const argv = result ? dealFunc(result[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;
}
})();