// ==UserScript== // @name Wenku Doc Downloader // @namespace http://tampermonkey.net/ // @version 1.8.13 // @description 对文档截图,合并为纯图片PDF。有限地支持(1)豆丁网(2)道客巴巴(3)360个人图书馆(4)得力文库(5)MBA智库(6)爱问文库(7)原创力文档(8)读根网(9)国标网(10)安全文库网(11)人人文库(12)云展网(13)360文库(14)技工教育网(15)文库吧(16)中国社会科学文库(17)金锄头(18)自然资源标准。预览多少页,导出多少页。额外支持(1)食典通(2)JJG 计量技术规范,详见下方说明。 // @author 2690874578@qq.com // @match *://*.docin.com/p-* // @match *://docimg1.docin.com/?wk=true // @match *://ishare.iask.sina.com.cn/f/* // @match *://ishare.iask.com/f/* // @match *://swf.ishare.down.sina.com.cn/?path=* // @match *://swf.ishare.down.sina.com.cn/?wk=true // @match *://www.deliwenku.com/p-* // @match *://file.deliwenku.com/?num=* // @match *://file3.deliwenku.com/?num=* // @match *://www.doc88.com/p-* // @match *://www.360doc.com/content/* // @match *://doc.mbalib.com/view/* // @match *://www.dugen.com/p-* // @match *://max.book118.com/html/* // @match *://openapi.book118.com/?* // @match *://view-cache.book118.com/pptView.html?* // @match *://*.book118.com/?readpage=* // @match *://c.gb688.cn/bzgk/gb/showGb?* // @match *://www.safewk.com/p-* // @match *://www.renrendoc.com/paper/* // @match *://www.renrendoc.com/p-* // @match *://www.yunzhan365.com/basic/* // @match *://book.yunzhan365.com/*index.html* // @match *://www.bing.com/search?q=Bing+AI&showconv=1* // @match *://wenku.so.com/d/* // @match *://jg.class.com.cn/cms/resourcedetail.htm?contentUid=* // @match *://preview.imm.aliyuncs.com/index.html?url=*/jgjyw/* // @match *://www.wenkub.com/p-*.html* // @match *://www.sklib.cn/manuscripts/?* // @match *://www.jinchutou.com/shtml/view-* // @match *://www.jinchutou.com/p-* // @match *://www.nrsis.org.cn/*/read/* // @require https://cdn.staticfile.org/jspdf/2.5.1/jspdf.umd.min.js // @require https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js // @icon https://s2.loli.net/2022/01/12/wc9je8RX7HELbYQ.png // @icon64 https://s2.loli.net/2022/01/12/tmFeSKDf8UkNMjC.png // @grant none // @run-at document-idle // @license GPL-3.0-only // @create 2021-11-22 // @note 1. 优化文件切割函数 // @downloadURL none // ==/UserScript== (function () { 'use strict'; /** * 基于 window.postMessage 通信的套接字对象 */ class Socket { /** * 创建套接字对象 * @param {Window} target 目标窗口 */ constructor(target) { if (!(target.window && (target === target.window))) { console.log(target); throw new Error(`target is not a [Window Object]`); } this.target = target; this.connected = false; this.listeners = new Set(); } get [Symbol.toStringTag]() { return "Socket"; } /** * 向目标窗口发消息 * @param {*} message */ talk(message) { if (!this.target) { throw new TypeError( `socket.target is not a window: ${this.target}` ); } this.target.postMessage(message, "*"); } /** * 添加捕获型监听器,返回实际添加的监听器 * @param {Function} listener (e: MessageEvent) => {...} * @param {boolean} once 是否在执行后自动销毁,默认 false;如为 true 则使用自动包装过的监听器 * @returns {Function} listener */ listen(listener, once=false) { if (this.listeners.has(listener)) { return; } let real_listener = listener; // 包装监听器 if (once) { const self = this; function wrapped(e) { listener(e); self.notListen(wrapped); } real_listener = wrapped; } // 添加监听器 this.listeners.add(real_listener); window.addEventListener( "message", real_listener, true ); return real_listener; } /** * 移除socket上的捕获型监听器 * @param {Function} listener (e: MessageEvent) => {...} */ notListen(listener) { console.log(listener); console.log( "listener delete operation:", this.listeners.delete(listener) ); window.removeEventListener("message", listener, true); } /** * 检查对方来信是否为pong消息 * @param {MessageEvent} e * @param {Function} resolve */ _on_pong(e, resolve) { // 收到pong消息 if (e.data.pong) { this.connected = true; this.listeners.forEach( listener => listener.ping ? this.notListen(listener) : 0 ); console.log("Client: Connected!\n" + new Date()); resolve(this); } } /** * 向对方发送ping消息 * @returns {Promise} */ _ping() { return new Promise((resolve, reject) => { // 绑定pong检查监听器 const listener = this.listen( e => this._on_pong(e, resolve) ); listener.ping = true; // 5分钟后超时 setTimeout( () => reject(new Error(`Timeout Error during receiving pong (>5min)`)), 5 * 60 * 1000 ); // 发送ping消息 this.talk({ ping: true }); }); } /** * 检查对方来信是否为ping消息 * @param {MessageEvent} e * @param {Function} resolve */ _on_ping(e, resolve) { // 收到ping消息 if (e.data.ping) { this.target = e.source; this.connected = true; this.listeners.forEach( listener => listener.pong ? this.notListen(listener) : 0 ); console.log("Server: Connected!\n" + new Date()); // resolve 后期约状态无法回退 // 但后续代码仍可执行 resolve(this); // 回应pong消息 this.talk({ pong: true }); } } /** * 当对方来信是为ping消息时回应pong消息 * @returns {Promise} */ _pong() { return new Promise(resolve => { // 绑定ping检查监听器 const listener = this.listen( e => this._on_ping(e, resolve) ); listener.pong = true; }); } /** * 连接至目标窗口 * @param {boolean} talk_first 是否先发送ping消息 * @param {Window} target 目标窗口 * @returns {Promise} */ connect(talk_first) { // 先发起握手 if (talk_first) { return this._ping(); } // 后发起握手 return this._pong(); } } const base = { Socket, /** * Construct a table with table[i] as the length of the longest prefix of the substring 0..i * @param {Array} arr * @returns {Array} */ longest_prefix: function(arr) { // create a table of size equal to the length of `str` // table[i] will store the prefix of the longest prefix of the substring str[0..i] let table = new Array(arr.length); let maxPrefix = 0; // the longest prefix of the substring str[0] has length table[0] = 0; // for the substrings the following substrings, we have two cases for (let i = 1; i < arr.length; i++) { // case 1. the current character doesn't match the last character of the longest prefix while (maxPrefix > 0 && arr[i] !== arr[maxPrefix]) { // if that is the case, we have to backtrack, and try find a character that will be equal to the current character // if we reach 0, then we couldn't find a chracter maxPrefix = table[maxPrefix - 1]; } // case 2. The last character of the longest prefix matches the current character in `str` if (arr[maxPrefix] === arr[i]) { // if that is the case, we know that the longest prefix at position i has one more character. // for example consider `-` be any character not contained in the set [a-c] // str = abc----abc // consider `i` to be the last character `c` in `str` // maxPrefix = will be 2 (the first `c` in `str`) // maxPrefix now will be 3 maxPrefix++; // so the max prefix for table[9] is 3 } table[i] = maxPrefix; } return table; }, // 用于取得一次列表中所有迭代器的值 getAllValus: function(iterators) { if (iterators.length === 0) { return [true, []]; } let values = []; for (let iterator of iterators) { let {value, done} = iterator.next(); if (done) { return [true, []]; } values.push(value); } return [false, values]; }, /** * 使用过时的execCommand复制文字 * @param {string} text */ oldCopy: function(text) { const input = document.createElement("input"); input.value = text; document.body.appendChild(input); input.select(); document.execCommand("copy"); input.remove(); }, b64ToUint6: function(nChr) { return nChr > 64 && nChr < 91 ? nChr - 65 : nChr > 96 && nChr < 123 ? nChr - 71 : nChr > 47 && nChr < 58 ? nChr + 4 : nChr === 43 ? 62 : nChr === 47 ? 63 : 0; }, /** * 元素选择器 * @param {string} selector 选择器 * @returns {Array} 元素列表 */ $: function(selector) { const self = this?.querySelectorAll ? this : document; return [...self.querySelectorAll(selector)]; }, /** * 安全元素选择器,直到元素存在时才返回元素列表,最多等待5秒 * @param {string} selector 选择器 * @returns {Promise>} 元素列表 */ $$: async function(selector) { const self = this?.querySelectorAll ? this : document; for (let i = 0; i < 10; i++) { let elems = [...self.querySelectorAll(selector)]; if (elems.length > 0) { return elems; } await new Promise(r => setTimeout(r, 500)); } throw Error(`"${selector}" not found in 5s`); }, /** * 将2个及以上的空白字符(除了换行符)替换成一个空格 * @param {string} text * @returns {string} */ stripBlanks: function(text) { return text .replace(/([^\r\n])(\s{2,})(?=[^\r\n])/g, "$1 ") .replace(/\n{2,}/, "\n"); }, /** * 复制属性(含访问器)到 target * @param {Object} target * @param {...Object} sources * @returns */ superAssign: function(target, ...sources) { sources.forEach(source => Object.defineProperties( target, Object.getOwnPropertyDescriptors(source) ) ); return target; }, makeCRC32: function() { function makeCRCTable() { let c; let crcTable = []; for(var n =0; n < 256; n++){ c = n; for(var k =0; k < 8; k++){ c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); } crcTable[n] = c; } return crcTable; } const crcTable = makeCRCTable(); /** * @param {string} str * @returns {number} */ return function(str) { let crc = 0 ^ (-1); for (var i = 0; i < str.length; i++ ) { crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF]; } return (crc ^ (-1)) >>> 0; }; } }; const box = `

Wenku Doc Downloader

`; const style = ` `; const popup = `
`; globalThis.wk$ = base.$; globalThis.wk$$ = base.$$; const utils = { Socket: base.Socket, PDF_LIB_URL: "https://cdn.staticfile.org/pdf-lib/1.17.1/pdf-lib.min.js", /** * Find all the patterns that matches in a given string `str` * this algorithm is based on the Knuth–Morris–Pratt algorithm. Its beauty consists in that it performs the matching in O(n) * @param {Array} arr * @param {Array} sub_arr * @returns {Array} */ kmp_matching: function(arr, sub_arr) { // find the prefix table in O(n) let prefixes = base.longest_prefix(sub_arr); let matches = []; // `j` is the index in `P` let j = 0; // `i` is the index in `S` let i = 0; while (i < arr.length) { // Case 1. S[i] == P[j] so we move to the next index in `S` and `P` if (arr[i] === sub_arr[j]) { i++; j++; } // Case 2. `j` is equal to the length of `P` // that means that we reached the end of `P` and thus we found a match if (j === sub_arr.length) { matches.push(i - j); // Next we have to update `j` because we want to save some time // instead of updating to j = 0 , we can jump to the last character of the longest prefix well known so far. // j-1 means the last character of `P` because j is actually `P.length` // e.g. // S = a b a b d e // P = `a b`a b // we will jump to `a b` and we will compare d and a in the next iteration // a b a b `d` e // a b `a` b j = prefixes[j - 1]; } // Case 3. // S[i] != P[j] There's a mismatch! else if (arr[i] !== sub_arr[j]) { // if we have found at least a character in common, do the same thing as in case 2 if (j !== 0) { j = prefixes[j - 1]; } else { // otherwise, j = 0, and we can move to the next character S[i+1] i++; } } } return matches; }, /** * 以指定原因弹窗提示并抛出错误 * @param {string} reason */ raise: function(reason) { alert(reason); throw new Error(reason); }, /** * 合并多个PDF * @param {Array} pdfs * @returns {Promise} */ join_pdfs: async function(pdfs) { if (!window.PDFLib) { await this.load_web_script(this.PDF_LIB_URL); } const combined = await PDFLib.PDFDocument.create(); for (const [i, buffer] of this.enumerate(pdfs)) { const pdf = await PDFLib.PDFDocument.load(buffer); const pages = await combined.copyPages( pdf, pdf.getPageIndices() ); for (const page of pages) { combined.addPage(page); } this.update_popup(`已经合并 ${i + 1} 组`); } return await combined.save(); }, /** * raise an error for status which is not in [200, 299] * @param {Response} response */ raise_for_status(response) { if (!response.ok) { throw new Error( `Fetch Error with status code: ${response.status}` ); } }, /** * 计算 str 的 CRC32 摘要(number) * @param {string} str * @returns {number} */ crc32: base.makeCRC32(), /** * 返回函数参数定义 * @param {Function} fn * @param {boolean} print 是否打印到控制台,默认 true * @returns {string | undefined} */ help: function(fn, print=true) { if (!(fn instanceof Function)) throw new Error(`fn must be a function`); const _fn = fn.__func__ || fn, ARROW_ARG = /^([^(]+?)=>/, FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m, STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, fn_text = Function.prototype.toString.call(_fn).replace(STRIP_COMMENTS, ''), args = fn_text.match(ARROW_ARG) || fn_text.match(FN_ARGS), // 如果自带 doc,优先使用,否则使用源码 doc = fn.__doc__ ? fn.__doc__ : args[0]; if (!print) return base.stripBlanks(doc); const color = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ) ; console.log("%c" + doc, `color: ${color}; font: small italic`); }, /** * 字节数组转十六进制字符串 * @param {Uint8Array} arr * @returns {string} */ hex_bytes: function(arr) { return Array.from(arr) .map(byte => byte.toString(16).padStart(2, "0")) .join(""); }, /** * 取得对象类型 * @param {*} obj * @returns {string} class */ classof: function(obj) { return Object .prototype .toString .call(obj) .slice(8, -1); }, /** * 随机改变字体颜色、大小、粗细 * @param {HTMLElement} elem */ emphasize_text: function(elem) { const rand = Math.random; elem.style.cssText = ` font-weight: ${200 + parseInt(700 * rand())}; font-size: ${(1 + rand()).toFixed(1)}em; color: hsl(${parseInt(360 * rand())}, ${parseInt(40 + 60 * rand())}%, ${parseInt(60 * rand())}%); background-color: yellow;`; }, /** * 等待直到 DOM 节点停止变化 * @param {HTMLElement} elem 监听节点 * @param {number} timeout 超时毫秒数 * @returns {Promise} observer */ until_stop: async function(elem, timeout=2000) { // 创建用于共享的监听器 let observer; // 创建超时 Promise const timeout_promise = new Promise((_, reject) => { setTimeout(() => { // 停止监听、释放资源 observer.disconnect(); const error = new Error( `Timeout Error occured on listening DOM mutation (max ${timeout}ms)`, { cause: elem } ); reject(error); }, timeout); }); // 开始元素节点变动监听 return Promise.race([ new Promise(resolve => { // 创建监听器 observer = new MutationObserver( (_, observer) => { // DOM 变动结束后终止监听、释放资源 observer.disconnect(); // 返回监听器 resolve(observer); } ); // 开始监听目标节点 observer.observe(elem, { subtree: true, childList: true, attributes: true }); }), timeout_promise, ]) .catch(error => { if (`${error}`.includes("Timeout Error")) { return observer; } console.error(error); throw error; }); }, /** * 返回子数组位置,找不到返回-1 * @param {Array} arr 父数组 * @param {Array} sub_arr 子数组 * @param {number} from 开始位置 * @returns {number} index */ index_of_sub_arr: function(arr, sub_arr, from) { // 如果子数组为空,则返回-1 if (sub_arr.length === 0) return -1; // 初始化当前位置为from let position = from; // 算出最大循环次数 const length = arr.length - sub_arr.length + 1; // 循环查找子数组直到没有更多 while (position < length) { // 如果当前位置的元素与子数组的第一个元素相等,则开始比较后续元素 if (arr[position] === sub_arr[0]) { // 初始化匹配标志为真 let match = true; // 循环比较后续元素,如果有不相等的,则将匹配标志设为假,并跳出循环 for (let i = 1; i < sub_arr.length; i++) { if (arr[position + i] !== sub_arr[i]) { match = false; break; } } // 如果匹配标志为真,则说明找到了子数组,返回当前位置 if (match) return position; } // 更新当前位置为下一个位置 position++; } // 如果循环结束还没有找到子数组,则返回-1 return -1; }, /** * 用文件头切断文件集合体 * @param {Uint8Array} bytes * @param {Uint8Array} head 默认 null,即使用 data 前 8 字节 * @returns {Array} */ split_files_by_head: function(bytes, head=null) { // 创建一个空数组用于存放结果 // const parts = []; // // 初始化当前位置为0 // let pos = 0; // let next_pos = 0; // // 确定 head // head = head || bytes.subarray(0, 8); // // 循环查找PNG标识直到没有更多 // while (next_pos < bytes.length) { // // 查找下一个PNG标识的位置 // next_pos = this.index_of_sub_arr( // bytes, head, pos + 1 // ); // let part; // if (next_pos === -1) { // // 如果没有找到,则说明已经到达最后一张图片,将其复制并添加到结果中 // part = bytes.subarray(pos); // parts.push(part); // break; // } // part = bytes.subarray(pos, next_pos); // parts.push(part); // // 更新当前位置为下一个PNG标识的位置 // pos = next_pos; // } const sub = bytes.subarray || bytes.slice; head = head || sub.call(bytes, 0, 8); const indexes = this.kmp_matching(bytes, head); const size = indexes.length; indexes.push(bytes.length); const parts = new Array(size); for (let i = 0; i < size; i++) { parts[i] = sub.call(bytes, indexes[i], indexes[i+1]); } // 返回结果数组 return parts; }, /** * 函数装饰器:仅执行一次 func */ once: function(fn) { let used = false; return function() { if (!used) { used = true; return fn(); } } }, /** * 返回一个包含计数器的迭代器, 其每次迭代值为 [index, value] * @param {Iterable} iterable * @returns */ enumerate: function* (iterable) { let i = 0; for (let value of iterable) { yield [i, value]; i++; } }, /** * 同步的迭代若干可迭代对象 * @param {...Iterable} iterables * @returns */ zip: function* (...iterables) { // 强制转为迭代器 let iterators = iterables.map( iterable => iterable[Symbol.iterator]() ); // 逐次迭代 while (true) { let [done, values] = base.getAllValus(iterators); if (done) { return; } if (values.length === 1) { yield values[0]; } else { yield values; } } }, /** * 返回指定范围整数生成器 * @param {number} end 如果只提供 end, 则返回 [0, end) * @param {number} end2 如果同时提供 end2, 则返回 [end, end2) * @param {number} step 步长, 可以为负数,不能为 0 * @returns */ range: function*(end, end2=null, step=1) { // 参数合法性校验 if (step === 0) { throw new RangeError("step can't be zero"); } const len = end2 - end; if (end2 && len && step && (len * step < 0)) { throw new RangeError(`[${end}, ${end2}) with step ${step} is invalid`); } // 生成范围 end2 = end2 === null ? 0 : end2; let [small, big] = [end, end2].sort((a, b) => a - b); // 开始迭代 if (step > 0) { for (let i = small; i < big; i += step) { yield i; } } else { for (let i = big; i > small; i += step) { yield i; } } }, /** * 获取整个文档的全部css样式 * @returns {string} css text */ get_all_styles: function() { let styles = []; for (let sheet of document.styleSheets) { let rules; try { rules = sheet.cssRules; } catch(e) { if (!(e instanceof DOMException)) { console.error(e); } continue; } for (let rule of rules) { styles.push(rule.cssText); } } return styles.join("\n\n"); }, /** * 复制text到剪贴板 * @param {string} text * @returns */ copy_text: function(text) { // 输出到控制台和剪贴板 console.log( text.length > 20 ? text.slice(0, 21) + "..." : text ); if (!navigator.clipboard) { base.oldCopy(text); return; } navigator.clipboard .writeText(text) .catch(_ => base.oldCopy(text)); }, /** * 复制媒体到剪贴板 * @param {Blob} blob */ copy: async function(blob) { const data = [new ClipboardItem({ [blob.type]: blob })]; try { await navigator.clipboard.write(data); console.log(`${blob.type} 成功复制到剪贴板`); } catch (err) { console.error(err.name, err.message); } }, /** * 创建并下载文件 * @param {string} file_name 文件名 * @param {ArrayBuffer | ArrayBufferView | Blob | string} content 内容 * @param {string} type 媒体类型,需要符合 MIME 标准 */ save: function(file_name, content, type="") { const blob = new Blob( [content], { type } ); const size = (blob.size / 1024).toFixed(1); console.log(`blob saved, size: ${size} kb, type: ${blob.type}`); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.download = file_name || "未命名文件"; a.href = url; a.click(); URL.revokeObjectURL(url); }, /** * 显示/隐藏按钮区 */ toggle_box: function() { let sec = wk$(".wk-box")[0]; if (sec.style.display === "none") { sec.style.display = "block"; return; } sec.style.display = "none"; }, /** * 异步地睡眠 delay 毫秒, 可选 max_delay 控制波动范围 * @param {number} delay 等待毫秒 * @param {number} max_delay 最大等待毫秒, 默认为null * @returns */ sleep: async function(delay, max_delay=null) { max_delay = max_delay === null ? delay : max_delay; delay = delay + (max_delay - delay) * Math.random(); return new Promise(resolve => setTimeout(resolve, delay)); }, /** * 允许打印页面 */ allow_print: function() { const style = document.createElement("style"); style.innerHTML = ` @media print { body { display: block; } }`; document.head.append(style); }, /** * 取得get参数key对应的value * @param {string} key * @returns {string} value */ get_param: function(key) { return new URL(location.href).searchParams.get(key); }, /** * 求main_set去除cut_set后的set * @param {Iterable} main_set * @param {Iterable} cut_set * @returns 差集 */ diff: function(main_set, cut_set) { const _diff = new Set(main_set); for (let elem of cut_set) { _diff.delete(elem); } return _diff; }, /** * 增强按钮(默认为蓝色按钮:展开文档)的点击效果 * @param {string} i 按钮序号 */ enhance_click: async function(i) { let btn = this.btn(i); const style = btn.getAttribute("style") || ""; // 变黑缩小 btn.setAttribute( "style", style + "color: black; font-weight: normal;" ); await utils.sleep(500); btn = this.btn(i); // 复原加粗 btn.setAttribute("style", style); }, /** * 绑定事件处理函数到指定按钮,返回实际添加的事件处理函数 * @param {(btn: HTMLButtonElement) => Promise} listener click监听器,第一个参数是按钮的引用 * @param {number} i 按钮序号 * @param {string} new_text 按钮的新文本,为null则不替换 * @returns {Function} 事件处理函数 */ onclick: function(listener, i, new_text=null) { const btn = this.btn(i); // 如果需要,替换按钮内文本 if (new_text) { btn.textContent = new_text; } const _listener = async () => { const btn = this.btn(i); await listener(btn); await this.enhance_click(i); btn.textContent = new_text; }; // 绑定事件,添加到页面上 btn.addEventListener("click", _listener, true); return _listener; }, /** * 返回第 index 个按钮引用 * @param {number} i * @returns {HTMLButtonElement} */ btn: function(i) { return wk$(`.wk-box [class="btn-${i}"]`)[0]; }, /** * 强制隐藏元素 * @param {string | Array} selector_or_elems */ force_hide: function(selector_or_elems) { const cls = "force-hide"; const elems = selector_or_elems instanceof Array ? selector_or_elems : wk$(selector_or_elems); elems.forEach(elem => { elem.classList.add(cls); }); // 判断css样式是否已经存在 let style = wk$(`style.${cls}`)[0]; // 如果已经存在,则无须重复创建 if (style) { return; } // 否则创建 style = document.createElement("style"); style.innerHTML = `style.${cls} { visibility: hidden !important; display: none !important; }`; document.head.append(style); }, /** * 等待直到元素可见。最多等待5秒。 * @param {HTMLElement} elem 一个元素 * @returns {Promise} elem */ until_visible: async function(elem) { let [max, i] = [25, 0]; let style = getComputedStyle(elem); // 如果不可见就等待0.2秒/轮 while (i <= max && (style.display === "none" || style.visibility !== "hidden") ) { i++; style = getComputedStyle(elem); await this.sleep(200); } return elem; }, /** * 等待直到函数返回true * @param {Function} isReady 判断条件达成与否的函数 * @param {number} timeout 最大等待秒数, 默认5000毫秒 */ wait_until: async function(isReady, timeout=5000) { const gap = 200; let chances = parseInt(timeout / gap); chances = chances < 1 ? 1 : chances; while (! await isReady()) { await this.sleep(200); chances -= 1; if (!chances) { break; } } }, /** * 隐藏按钮,打印页面,显示按钮 */ print_page: function() { // 隐藏按钮,然后打印页面 this.toggle_box(); setTimeout(print, 500); setTimeout(this.toggle_box, 1000); }, /** * 切换按钮显示/隐藏状态 * @param {number} i 按钮序号 * @returns 按钮元素的引用 */ toggle_btn: function(i) { const btn = this.btn(i); const display = getComputedStyle(btn).display; if (display === "none") { btn.style.display = "block"; } else { btn.style.display = "none"; } return btn; }, /** * 用input框跳转到对应页码 * @param {HTMLInputElement} input 当前页码 * @param {string | number} page_num 目标页码 * @param {string} type 键盘事件类型:"keyup" | "keypress" | "keydown" */ to_page: function(input, page_num, type) { // 设置跳转页码为目标页码 input.value = `${page_num}`; // 模拟回车事件来跳转 const enter = new KeyboardEvent(type, { bubbles: true, cancelable: true, keyCode: 13 }); input.dispatchEvent(enter); }, /** * 判断给定的url是否与当前页面同源 * @param {string} url * @returns {boolean} */ is_same_origin: function(url) { url = new URL(url); if (url.protocol === "data:") { return true; } if (location.protocol === url.protocol && location.host === url.host && location.port === url.port ) { return true; } return false; }, /** * 在新标签页打开链接,如果提供文件名则下载 * @param {string} url * @param {string} fname 下载文件的名称,默认为空,代表不下载 */ open_in_new_tab: function(url, fname="") { const a = document.createElement("a"); a.href = url; a.target = "_blank"; if (fname && this.is_same_origin(url)) { a.download = fname; } a.click(); }, /** * 用try移除元素 * @param {HTMLElement} element 要移除的元素 */ remove: function(element) { try { element.remove(); } catch (e) {} }, /** * 用try移除若干元素 * @param {Iterable} elements 要移除的元素列表 */ remove_multi: function(elements) { for (const elem of elements) { this.remove(elem); } }, /** * 使文档在页面上居中 * @param {string} selector 文档容器的css选择器 * @param {string} default_offset 文档部分向右偏移的百分比(0-59) * @returns 偏移值是否合法 */ centre: function(selector, default_offset) { const elem = wk$(selector)[0]; const offset = prompt("请输入偏移百分位:", default_offset); // 如果输入的数字不在 0-59 内,提醒用户重新设置 if (offset.length === 1 && offset.search(/[0-9]/) !== -1) { elem.style.marginLeft = offset + "%"; return true; } if (offset.length === 2 && offset.search(/[1-5][0-9]/) !== -1) { elem.style.marginLeft = offset + "%"; return true; } alert("请输入一个正整数,范围在0至59之间,用来使文档居中"); return false; }, /** * 等待全部任务落定后返回值的列表 * @param {Iterable} tasks * @returns {Promise} values */ gather: async function(tasks) { const results = await Promise.allSettled(tasks); return results .filter(result => result.value) .map(result => result.value); }, /** * html元素列表转为canvas列表 * @param {Array} elements * @returns {Promise>} */ elems_to_canvases: async function(elements) { if (!globalThis.html2canvas) { await this.load_web_script( "https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js" ); } // 如果是空列表, 则抛出异常 if (elements.length === 0) { throw new Error("htmlToCanvases 未得到任何html元素"); } return this.gather( elements.map(html2canvas) ); }, /** * 将html元素转为canvas再合并到pdf中,最后下载pdf * @param {Array} elements 元素列表 * @param {string} title 文档标题 */ elems_to_pdf: async function(elements, title="文档") { // 如果是空元素列表,终止函数 const canvases = await this.elems_to_canvases(elements); // 控制台检查结果 console.log("生成的canvas元素如下:"); console.log(canvases); // 合并为PDF this.imgs_to_pdf(canvases, title); }, /** * 使用xhr异步GET请求目标url,返回响应体blob * @param {string} url * @returns {Promise} blob */ xhr_get_blob: async function(url) { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = "blob"; return new Promise((resolve, reject) => { xhr.onload = () => { const code = xhr.status; if (code >= 200 && code <= 299) { resolve(xhr.response); } else { reject(new Error(`Network Error: ${code}`)); } }; xhr.send(); }); }, /** * 加载CDN脚本 * @param {string} url */ load_web_script: async function(url) { try { // xhr+eval方式 Function( await (await this.xhr_get_blob(url)).text() )(); } catch(e) { console.error(e); // 嵌入