Repository navigation
Expand file tree
/
Copy pathsave-single-html-file.user.js
More file actions
181 lines (171 loc) · 7.48 KB
/
Copy pathsave-single-html-file.user.js
File metadata and controls
181 lines (171 loc) · 7.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
// ==UserScript==
// @name SingleFile 保存HTML(官方核心版)
// @namespace https://github.com/gildas-lormeau/SingleFile
// @version 1.5.2
// @description 基于 SingleFile 官方核心,将当前网页保存为单个自包含 HTML 文件(图片/CSS/字体全部内联)。核心通过 @resource 作为脚本资源加载(GM_getResourceText 读取后注入执行);脚本本体小巧、未压缩、可读;通过 GM_xmlhttpRequest 桥接 fetch 以绕过跨域限制。脚本菜单手动执行
// @author SingleFile (gildas-lormeau) + 油猴封装
// @license AGPL-3.0 (SingleFile core) / MIT (本封装代码)
// @match *://*/*
// @run-at document-idle
// @resource SF_CORE https://fastly.jsdelivr.net/gh/gildas-lormeau/SingleFile@master/lib/single-file.js
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_registerMenuCommand
// @grant GM_getResourceText
// @connect *
// @noframes
// @downloadURL https://update.greasyfork.org/scripts/600937/SingleFile%20%E4%BF%9D%E5%AD%98HTML%EF%BC%88%E5%AE%98%E6%96%B9%E6%A0%B8%E5%BF%83%E7%89%88%EF%BC%89.user.js
// @updateURL https://update.greasyfork.org/scripts/600937/SingleFile%20%E4%BF%9D%E5%AD%98HTML%EF%BC%88%E5%AE%98%E6%96%B9%E6%A0%B8%E5%BF%83%E7%89%88%EF%BC%89.meta.js
// ==/UserScript==
(function () {
'use strict';
/* 核心资源通过 @resource 声明(默认用 fastly.jsdelivr.net),运行时用 GM_getResourceText 读取。
若它在你的网络不可用,可把上面 @resource 的域名替换为其他 jsDelivr 镜像(任选其一):
https://gcore.jsdelivr.net/gh/gildas-lormeau/SingleFile@master/lib/single-file.js
https://testingcf.jsdelivr.net/gh/gildas-lormeau/SingleFile@master/lib/single-file.js
https://cdn.jsdelivr.net/gh/gildas-lormeau/SingleFile@master/lib/single-file.js
资源在脚本安装/更新时由油猴缓存;如需强制刷新资源,可在油猴管理面板更新脚本。 */
/* getPageData 选项(与官方扩展一致)。图片默认内联为 data URI。 */
const OPTIONS = {
removeHiddenElements: true,
removeUnusedStyles: true,
removeUnusedFonts: true,
removeImports: true,
blockScripts: true,
blockAudios: true,
blockVideos: true,
removeAlternativeFonts: true,
removeAlternativeMedias: true,
removeAlternativeImages: true,
groupDuplicateImages: true,
};
/* fetch 桥接:用 GM_xmlhttpRequest 代替原生 fetch 绕过 CORS,
使核心能抓取跨域图片 / CSS / 字体并内联。blob:/data: 本地资源交给原生 fetch。 */
function parseHeaders(raw) {
const map = new Map();
if (raw) raw.split(/\r?\n/).forEach((line) => {
const idx = line.indexOf(':');
if (idx > 0) {
const k = line.slice(0, idx).trim().toLowerCase();
const v = line.slice(idx + 1).trim();
if (k) map.set(k, v);
}
});
return {
get: (n) => (n ? map.get(String(n).toLowerCase()) : undefined),
has: (n) => (n ? map.has(String(n).toLowerCase()) : false),
};
}
function gmFetch(url, init) {
init = init || {};
if (/^(blob:|data:)/i.test(url)) return fetch(url, init);
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: (init.method || 'GET').toUpperCase(),
url: url, headers: init.headers || {}, responseType: 'arraybuffer', timeout: 30000,
onload: (resp) => {
let buf = resp.response;
if (!(buf instanceof ArrayBuffer)) buf = new Uint8Array(buf).buffer;
resolve({
ok: resp.status >= 200 && resp.status < 300,
status: resp.status, statusText: resp.statusText || '',
url: resp.finalUrl || url,
headers: parseHeaders(resp.responseHeaders),
arrayBuffer: () => Promise.resolve(buf),
});
},
onerror: (e) => reject(new Error('GM fetch 失败: ' + (e && e.error ? e.error : e))),
ontimeout: () => reject(new Error('GM fetch 超时: ' + url)),
});
});
}
/* 文件名:以网页标题命名,后缀 .HTML */
function sanitize(name) {
return (name || 'webpage')
.trim()
.replace(/\s+/g, ' ')
.replace(/[\\/:*?"<>|]+/g, '_')
.slice(0, 120) || 'webpage';
}
function download(content, filename) {
const blob = new Blob([content], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 60000);
}
/* 状态输出:仅写入控制台,不渲染任何按钮 / 提示条 / 动画 */
function toast(msg) { console.log('[SingleFile] ' + msg); }
/* 从脚本资源读取核心并注入页面执行 */
function loadCore() {
if (window.singlefile && window.singlefile.getPageData) return window.singlefile;
let code;
try {
code = GM_getResourceText('SF_CORE');
} catch (e) {
console.error('[SingleFile] 读取资源失败:', e);
return null;
}
if (!code) {
console.error('[SingleFile] 资源 SF_CORE 为空:请确认 @resource 的 URL 可访问并在油猴中更新脚本');
return null;
}
try {
// 在页面上下文注入执行(通过 script 标签),以挂载 window.singlefile
const s = document.createElement('script');
s.textContent = code;
(document.head || document.documentElement).appendChild(s);
s.remove();
} catch (e) {
console.error('[SingleFile] 注入核心失败:', e);
return null;
}
return window.singlefile || null;
}
/* 抓取并保存 */
async function capture() {
const sf = window.singlefile || globalThis.singlefile;
if (!sf || !sf.getPageData) { toast('SingleFile 核心未加载'); return; }
toast('SingleFile 处理中…');
try {
const data = await sf.getPageData(OPTIONS);
let content = data.content;
if (content instanceof Uint8Array) content = new TextDecoder('utf-8').decode(content);
const name = sanitize(document.title) + '.HTML';
download(content, name);
toast('已保存:' + name);
} catch (e) {
toast('保存失败:' + (e && e.message ? e.message : e));
console.error('[SingleFile]', e);
}
}
/* ---------------------------------------------------------------- *
* 启动:从 @resource 读取核心并注入 → 注入 fetch 桥接 → 注册脚本菜单命令。
* 不显示任何按钮 / 动画。
* ---------------------------------------------------------------- */
(function bootstrap() {
const sf = loadCore();
if (!sf || !sf.getPageData) {
console.error('[SingleFile] 核心未加载:请确认 @resource 的 jsDelivr 核心 URL 可访问');
return;
}
try {
// 注入自定义 fetch,绕过跨域限制
sf.init({ fetch: gmFetch });
} catch (e) {
// 部分版本 init 为可选,忽略
console.warn('[SingleFile] init 跳过:', e);
}
// 通过油猴脚本菜单「保存为单文件」触发;不显示任何按钮 / 动画
GM_registerMenuCommand('保存为单文件', capture, 's');
// 可选热键:Alt + S
document.addEventListener('keydown', (ev) => {
if (ev.altKey && (ev.key === 's' || ev.key === 'S')) {
ev.preventDefault();
capture();
}
});
console.log('[SingleFile] 就绪:在油猴图标菜单中选择「保存为单文件」,或按 Alt+S');
})();
})();