您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
markdown grabber
// ==UserScript== // @name Markdown Grabber // @namespace http://tampermonkey.net/ // @version 1.1 // @description markdown grabber // @author 5ec1cff // @match *://*/* // @license AGPL // @grant unsafeWindow // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @connect * // ==/UserScript== // 2021.12.24 Fri: 修正 // 2022.03.15 Tue: 增加下载图片支持(默认启用) // 2023.03.01 Wed: 支持 xz.aliyun.cn ;下载图片附带 Referer // 2025.06.30 Mon: 兼容 bbs.kanxue.com ,支持代码,支持解密链接 (function () { 'use strict'; if (window.top !== window) return; // 阻止在 iframe 启用 const downloadPics = true; const picMap = new Map(); const console = unsafeWindow.console.context(); function decodeKanxueUrl(s) { let str = s; try { // 检查是否已经是解密后的URL格式 if (str.includes('http://') || str.includes('https://') || str.includes('www.')) { return str; } // 1. 移除salt const encrypted = str.slice(3); // 2. 定义替换规则的反向映射 const replacePairs = { 'K9': 'a', 'L8': 'b', 'M7': 'c', 'N6': 'd', 'P5': 'e', 'Q4': 'f', 'R3': 'g', 'S2': 'h', 'T1': 'i', 'U0': 'j', 'V1': 'k', 'W2': 'l', 'X3': 'm', 'Y4': 'n', 'Z5': 'o', 'A6': 'p', 'B7': 'q', 'C8': 'r', 'D9': 's', 'E0': 't', 'F1': 'u', 'G2': 'v', 'H3': 'w', 'I4': 'x', 'J5': 'y', 'K6': 'z', 'l9': 'A', 'm8': 'B', 'n7': 'C', 'o6': 'D', 'p5': 'E', 'q4': 'F', 'r3': 'G', 's2': 'H', 't1': 'I', 'u0': 'J', 'v1': 'K', 'w2': 'L', 'x3': 'M', 'y4': 'N', 'z5': 'O', 'a6': 'P', 'b7': 'Q', 'c8': 'R', 'd9': 'S', 'e0': 'T', 'f1': 'U', 'g2': 'V', 'h3': 'W', 'i4': 'X', 'j5': 'Y', 'k6': 'Z', '@1': '0', '#2': '1', '$3': '2', '%4': '3', '^5': '4', '&6': '5', '*7': '6', '(8': '7', ')9': '8', '!0': '9', '-_': '+', '|~': '/', '`.': '=' }; // 3. 按长度排序替换规则 const sortedPairs = Object.entries(replacePairs) .sort((a, b) => b[0].length - a[0].length); // 4. 执行替换 let base64 = encrypted; for (const [pattern, replacement] of sortedPairs) { base64 = base64.split(pattern).join(replacement); } // 5. 补全base64字符串的等号 while (base64.length % 4) { base64 += '='; } // 6. 清理非法字符 base64 = base64.replace(/[^A-Za-z0-9+/=]/g, ''); // 7. base64解码 const decoded = atob(base64); s = decoded; } catch (e) { console.log('decodeKanxueUrl', e); return str; // 如果解密失败,返回原始字符串 } s = s.replace(/_/g, "%"); s = decodeURIComponent(s); return s; } function getPictureKey(url) { return url; } function getPicture(url) { const key = getPictureKey(url); if (picMap.get(url) == null) { picMap.set(url, new Promise((rs, rj) => { if (!downloadPics || !url?.startsWith("http")) { rs([key, url]); return; } GM_xmlhttpRequest({ url: url, headers: { Referer: location.href }, responseType: "blob", onload(r) { const fr = new FileReader(); fr.onloadend = () => { console.log('load done:', url); rs([key, fr.result]); } fr.onerror = (e) => { rj(e); } fr.readAsDataURL(r.response); }, onerror(e) { rj(e); }, onabort(e) { rj(e); } }) }) ); } return key; } function parseSimpleStyle(e) { let r = ''; switch (e.tagName.toLowerCase()) { case 'b': case 'strong': r += `**${parseSingleLine(e)}**`; break; case 'i': case 'em': r += `*${parseSingleLine(e)}*`; break; case 's': case 'strike': r += `~~${parseSingleLine(e)}~~`; break; case 'a': { if (e.href) { let href = e.getAttribute('href'); if (href.indexOf('elink@') !== -1) { href = decodeKanxueUrl(href.substr(6)); } r += `[${parseSingleLine(e)}](${href})`; } break; } case 'code': r += `\`${e.innerText}\``; break; case 'img': r += `\n![][${getPicture(e.src)}]\n`; break; default: r += parseSingleLine(e); } return r; } function parseSingleLine(element) { if (element instanceof Text) return element.data.trim(); let r = ''; if (element instanceof HTMLElement) { for (let e of element.childNodes) { if (e instanceof Text) r += e.data; if (!(e instanceof HTMLElement)) continue; r += parseSimpleStyle(e); } } return r.trim(); } function isSingleLine(node) { return !node.querySelector('p,ul,ol,br'); } function parseNode(element) { let lines = [], singleLine = null; if (element instanceof HTMLElement) { for (let e of element.childNodes) { if (!(e instanceof HTMLElement) && !(e instanceof Text)) continue; let tagName; if (e instanceof Text) { tagName = 'TEXT'; } else { tagName = e.tagName.toLowerCase(); } switch (tagName) { case 'TEXT': case 'a': case 'b': case 'strong': case 'i': case 'em': case 's': case 'strike': case 'a': case 'code': { if (singleLine == null) singleLine = ''; if (tagName == 'TEXT') { singleLine += e.data.trim(); } else { singleLine += parseSimpleStyle(e); } continue; } default: if (singleLine != null) { lines.push(singleLine); singleLine = null; } } switch (tagName) { // ignores case 'button': case 'style': case 'header': case 'script': continue; case 'p': lines.push(parseSingleLine(e) + '\n'); break; case 'br': lines.push('\n'); break; case 'ul': case 'ol': { lines.push(''); let is_order = tagName == 'ol', j = 1; for (let item of e.childNodes) { let pref = is_order ? `${j}. ` : `- `; if (item instanceof HTMLLIElement) { if (!isSingleLine(item)) { let item_lines = parseNode(item); for (let i = 0; i < item_lines.length; i++) { const l = item_lines[i].trim() if (l) { lines.push(`${i==0?pref:' '}${item_lines[i]}`); } } } else { lines.push(`${pref}${parseSingleLine(item)}`); } j++; } } lines.push(''); break; } case 'pre': { // debugger lines.push('```'); lines.push(...(e.querySelector('code') || e).innerText.trim().split('\n')); lines.push('```'); break; } case 'blockquote': { lines.push(''); let item_lines = parseNode(e); for (let i = 0; i < item_lines.length; i++) { lines.push(`> ${item_lines[i]}`); } lines.push(''); break; } case 'table': { if (e.classList.contains('syntaxhighlighter')) { let content = e.querySelector('.code'); if (content) { let lang = Array.from(e.classList).filter(x=>x!='syntaxhighlighter')[0] || ''; lines.push('```' + lang); lines.push(...content.innerText.trim().split('\n')); lines.push('```'); break; } } lines.push(''); let head = e.querySelector('thead'); if (!head) { console.warn('unknown table!'); // resolve body as normal tag let body; if (body = e.querySelector('tbody')) { lines.push(...parseNode(body)); } continue; } let head_line = '|', sep_line = '|'; for (let h of head.querySelectorAll('th')) { head_line += `${parseSingleLine(h)}|`; sep_line += `--|` } lines.push(head_line); lines.push(sep_line); let body = e.querySelector('tbody'); for (let b of body.querySelectorAll('tr')) { let line = '|'; for (let d of b.querySelectorAll('td')) { line += `${parseSingleLine(d)}|`; } lines.push(line); } lines.push(''); break; } case 'hr': lines.push('\n---\n'); break; case 'img': lines.push(`\n![][${getPicture(e.src)}]\n`); break; case 'figure': { if (e.classList.contains('highlight')) { let lang = e.classList[1] || ''; let code = e.querySelector('td.code pre'); if (code != null) { lines.push('```' + lang); lines.push(...code.innerText.trim().split('\n')); lines.push('```'); break; } } // fallthrough } case 'td': { if (e.classList.contains('gutter')) continue; // fallthrough } default: { let r; if (r = tagName.match(/h(\d+)/)) { lines.push(`\n${'#'.repeat(Number(r[1]))} ${parseSingleLine(e)} \n`); } else { lines.splice(lines.length, 0, ...parseNode(e)); } } } } if (singleLine != null) lines.push(singleLine); } return lines; } function findArticle() { let article = document.body.querySelector('article'); if (article) return article; article = document.body.querySelector('div.markdown-body,div.mod-content'); if (article) return article; article = document.body.querySelector('div.message_md_type'); // kanxue if (article) return article; let maxChild = 0, node = null; for (let n of document.querySelectorAll('h1')) { if (n.parentNode && n.parentNode.childElementCount >= maxChild) { node = n.parentNode; } } return node; } async function html2MD() { let article = findArticle(); let title = document.querySelector('h1'); let r = ''; if (title) { r += `# ${parseSingleLine(title)}`; } else { r += `# ${document.title}`; } r += `\n${location.href}\n\n`; r += await nodeToMD(article); return r; } async function nodeToMD(node) { picMap.clear(); let r = ''; let lines = parseNode(node); for (let l of lines) { r += `${l}\n`; } let pics = await Promise.race([ Promise.all(picMap.values()), new Promise((_, rj) => { console.log("waiting 10s for downloading pictures...", picMap.size); setTimeout(() => { rj('time out!'); }, 10000) }) ]); r += '\n'; for (let [key, url] of pics) { r += `[${key}]:${url}\n`; } return r; } unsafeWindow.md = nodeToMD; // unsafeWindow.__xhr = GM_xmlhttpRequest; // unsafeWindow._getpic = getPicture async function onClick() { let url = URL.createObjectURL(new Blob([await html2MD()], { type: 'text/plain' })); let a = document.createElement('a'); a.download = `${document.title}.md`; a.href = url; document.body.append(a); a.click(); a.remove(); } GM_registerMenuCommand('下载 Markdown', () => { onClick(); }) })();