您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
将微信公众号文章转换为Markdown格式并下载
// ==UserScript== // @name 微信公众号文章转Markdown // @namespace http://tampermonkey.net/ // @version 1.2.0 // @description 将微信公众号文章转换为Markdown格式并下载 // @author BigShuaiBee // @match https://mp.weixin.qq.com/s/* // @match https://mp.weixin.qq.com/s?* // @match https://mp.weixin.qq.com/cgi-bin/readtemplate* // @grant none // @license MIT // @run-at document-end // ==/UserScript== (function() { 'use strict'; /** * 创建下载按钮 */ function createDownloadButton() { console.log('[微信转MD] 开始创建下载按钮'); // 检查是否已经存在按钮 if (document.getElementById('markdown-download-btn')) { console.log('[微信转MD] 按钮已存在,跳过创建'); return; } const button = document.createElement('button'); button.id = 'markdown-download-btn'; button.innerHTML = '📄 下载为Markdown'; button.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 9999; background: #07c160; color: white; border: none; padding: 12px 20px; border-radius: 6px; font-size: 14px; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.2); transition: all 0.3s ease; `; // 悬停效果 button.addEventListener('mouseenter', () => { button.style.background = '#06ad56'; button.style.transform = 'translateY(-2px)'; }); button.addEventListener('mouseleave', () => { button.style.background = '#07c160'; button.style.transform = 'translateY(0)'; }); // 点击事件 button.addEventListener('click', convertToMarkdown); // 添加到页面 document.body.appendChild(button); console.log('[微信转MD] 下载按钮创建成功'); } /** * 获取文章标题 * @returns {string} 文章标题 */ function getArticleTitle() { const titleElement = document.querySelector('#activity-name, .rich_media_title, h1'); return titleElement ? titleElement.textContent.trim() : '微信文章'; } /** * 获取文章作者 * @returns {string} 文章作者 */ function getArticleAuthor() { const authorElement = document.querySelector('#js_name, .rich_media_meta_text, .profile_nickname'); return authorElement ? authorElement.textContent.trim() : ''; } /** * 获取发布时间 * @returns {string} 发布时间 */ function getPublishTime() { const timeElement = document.querySelector('#publish_time, .rich_media_meta_text'); if (timeElement) { const timeText = timeElement.textContent.trim(); // 匹配时间格式 const timeMatch = timeText.match(/\d{4}-\d{2}-\d{2}|\d{4}年\d{1,2}月\d{1,2}日/); return timeMatch ? timeMatch[0] : ''; } return ''; } /** * 处理图片元素 * @param {Element} img 图片元素 * @returns {string} Markdown格式的图片 */ function processImage(img) { // 尝试多种属性获取图片URL const urlAttributes = ['data-src', 'data-original', 'data-lazy-src', 'data-url', 'src', 'data-original-src', 'data-actualsrc']; let imageUrl = ''; let usedAttribute = ''; for (const attr of urlAttributes) { const url = img.getAttribute(attr); if (url && url.trim() && !url.startsWith('data:image/svg') && !url.includes('loading')) { imageUrl = url.trim(); usedAttribute = attr; break; } } // 如果还是没找到,尝试从父元素或相关元素中查找 if (!imageUrl) { const parent = img.parentElement; if (parent) { // 检查父元素的背景图片 const bgImage = window.getComputedStyle(parent).backgroundImage; if (bgImage && bgImage !== 'none') { const match = bgImage.match(/url\(["']?([^"'\)]+)["']?\)/); if (match && match[1]) { imageUrl = match[1]; usedAttribute = 'background-image'; } } // 检查父元素的data属性 if (!imageUrl) { for (const attr of urlAttributes) { const url = parent.getAttribute(attr); if (url && url.trim() && !url.startsWith('data:image/svg')) { imageUrl = url.trim(); usedAttribute = `parent.${attr}`; break; } } } } } if (!imageUrl) { console.log('[微信转MD] 图片URL获取失败,图片属性:', { src: img.getAttribute('src'), 'data-src': img.getAttribute('data-src'), 'data-original': img.getAttribute('data-original'), 'data-lazy-src': img.getAttribute('data-lazy-src'), 'data-url': img.getAttribute('data-url'), className: img.className, parentClassName: img.parentElement?.className, outerHTML: img.outerHTML.substring(0, 300) }); return ''; } console.log(`[微信转MD] 找到图片URL: ${imageUrl} (来源: ${usedAttribute})`); const alt = img.getAttribute('alt') || img.getAttribute('title') || '图片'; return `\n\n`; } /** * 处理链接元素 * @param {Element} link 链接元素 * @returns {string} Markdown格式的链接 */ function processLink(link) { const href = link.href; const text = link.textContent.trim(); return `[${text}](${href})`; } /** * 处理文本样式 * @param {Element} element 元素 * @returns {string} 处理后的文本 */ function processTextStyle(element) { let text = element.textContent.trim(); // 处理加粗 if (element.style.fontWeight === 'bold' || element.tagName === 'STRONG' || element.tagName === 'B') { text = `**${text}**`; } // 处理斜体 if (element.style.fontStyle === 'italic' || element.tagName === 'EM' || element.tagName === 'I') { text = `*${text}*`; } // 处理删除线 if (element.style.textDecoration === 'line-through' || element.tagName === 'DEL' || element.tagName === 'S') { text = `~~${text}~~`; } return text; } /** * 处理代码块内容,保留原始格式 * @param {string} codeText 代码文本 * @returns {string} 处理后的代码文本 */ function processCodeContent(codeText) { // 保留所有原始格式,只做最基本的换行符统一 return codeText .replace(/\r\n/g, '\n') // 统一换行符为LF .replace(/\r/g, '\n') // 将CR转换为LF .replace(/\t/g, ' '); // 将制表符转换为4个空格,保持一致性 } /** * 从代码元素中提取文本,特别处理微信公众号的换行符问题 * @param {Element} element 代码元素 * @returns {string} 提取的代码文本 */ function extractCodeText(element) { let codeText = ''; let hasStructuredContent = false; // 递归遍历所有子节点,特别处理br标签和文本节点 function traverseNode(node, depth = 0) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent; // 直接添加文本内容,不做任何清理 codeText += text; } else if (node.nodeType === Node.ELEMENT_NODE) { const tagName = node.tagName.toLowerCase(); if (tagName === 'br') { // br标签转换为换行符 codeText += '\n'; hasStructuredContent = true; } else if (tagName === 'div' || tagName === 'p') { // div和p标签通常表示新行 if (codeText && !codeText.endsWith('\n')) { codeText += '\n'; } // 递归处理子节点 for (const child of node.childNodes) { traverseNode(child, depth + 1); } // 在div/p结束后添加换行符 if (!codeText.endsWith('\n')) { codeText += '\n'; } hasStructuredContent = true; } else if (tagName === 'span') { // span标签可能包含行内代码,检查是否有特殊样式 const style = node.style || {}; const className = node.className || ''; // 如果span有特殊的代码样式,可能需要特殊处理 if (className.includes('line') || style.display === 'block') { // 可能是代码行,在前后添加换行符 if (codeText && !codeText.endsWith('\n')) { codeText += '\n'; } for (const child of node.childNodes) { traverseNode(child, depth + 1); } if (!codeText.endsWith('\n')) { codeText += '\n'; } hasStructuredContent = true; } else { // 普通span,递归处理子节点 for (const child of node.childNodes) { traverseNode(child, depth + 1); } } } else { // 其他标签,递归处理子节点 for (const child of node.childNodes) { traverseNode(child, depth + 1); } } } } // 开始遍历 for (const child of element.childNodes) { traverseNode(child); } // 如果没有通过DOM遍历得到有意义的内容,回退到textContent if (!codeText.trim() || (!hasStructuredContent && codeText.indexOf('\n') === -1)) { const fallbackText = element.textContent || element.innerText || ''; // 如果textContent也没有换行符,尝试通过innerHTML分析 if (fallbackText.indexOf('\n') === -1) { const innerHTML = element.innerHTML; // 如果innerHTML包含br标签,手动替换 if (innerHTML.includes('<br')) { const processedHTML = innerHTML .replace(/<br\s*\/?>/gi, '\n') .replace(/<[^>]+>/g, '') // 移除其他HTML标签 .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&'); codeText = processedHTML; } else { codeText = fallbackText; } } else { codeText = fallbackText; } } // 应用基本的代码内容处理 return processCodeContent(codeText); } /** * 处理表格转换 * @param {Element} table 表格元素 * @returns {string} Markdown表格 */ function processTable(table) { const rows = table.querySelectorAll('tr'); if (rows.length === 0) return ''; let markdown = '\n'; let isFirstRow = true; rows.forEach(row => { const cells = row.querySelectorAll('th, td'); if (cells.length === 0) return; // 构建表格行 const cellContents = Array.from(cells).map(cell => { return cell.textContent.trim().replace(/\|/g, '\\|'); }); markdown += '| ' + cellContents.join(' | ') + ' |\n'; // 如果是第一行(表头),添加分隔符 if (isFirstRow) { const separator = Array.from(cells).map(() => '---').join(' | '); markdown += '| ' + separator + ' |\n'; isFirstRow = false; } }); return markdown; } /** * 转换HTML元素为Markdown * @param {Element} element HTML元素 * @returns {string} Markdown文本 */ function htmlToMarkdown(element) { let markdown = ''; for (const node of element.childNodes) { if (node.nodeType === Node.TEXT_NODE) { let text = node.textContent; // 检查父元素链是否包含代码相关的元素 const isInCodeBlock = (function() { let current = node.parentElement; while (current) { const tagName = current.tagName.toLowerCase(); const className = current.className || ''; // 检查标签名 if (tagName === 'pre' || tagName === 'code') { return true; } // 检查类名 if (className.includes('code') || className.includes('highlight') || className.includes('prism') || className.includes('hljs') || className.includes('language-') || className.includes('lang-') || className.includes('prettyprint') || className.includes('syntax')) { return true; } // 检查data属性 if (current.hasAttribute('data-lang') || current.hasAttribute('data-language') || current.hasAttribute('data-code')) { return true; } current = current.parentElement; } return false; })(); if (isInCodeBlock) { // 在代码块中,保留所有空白字符和换行符 markdown += text; } else { // 在普通文本中,保留必要的空白字符,但清理多余的空白 text = text.replace(/\s+/g, ' '); // 如果文本前后有换行符,保留它们 if (node.textContent.startsWith('\n') || node.textContent.startsWith('\r')) { text = '\n' + text.trim(); } if (node.textContent.endsWith('\n') || node.textContent.endsWith('\r')) { text = text.trim() + '\n'; } markdown += text; } } else if (node.nodeType === Node.ELEMENT_NODE) { const tagName = node.tagName.toLowerCase(); switch (tagName) { case 'h1': markdown += `\n# ${htmlToMarkdown(node).trim()}\n\n`; break; case 'h2': markdown += `\n## ${htmlToMarkdown(node).trim()}\n\n`; break; case 'h3': markdown += `\n### ${htmlToMarkdown(node).trim()}\n\n`; break; case 'h4': markdown += `\n#### ${htmlToMarkdown(node).trim()}\n\n`; break; case 'h5': markdown += `\n##### ${htmlToMarkdown(node).trim()}\n\n`; break; case 'h6': markdown += `\n###### ${htmlToMarkdown(node).trim()}\n\n`; break; case 'p': const pContent = htmlToMarkdown(node).trim(); if (pContent) { markdown += `\n\n${pContent}\n\n`; } break; case 'br': markdown += ' \n'; // 使用两个空格+换行符来强制换行 break; case 'img': markdown += processImage(node) + '\n\n'; break; case 'a': // 检查链接前是否需要换行 const linkText = processLink(node); // 如果前面不是空白字符,添加空格分隔 if (markdown && !markdown.endsWith(' ') && !markdown.endsWith('\n')) { markdown += ' '; } markdown += linkText; break; case 'strong': case 'b': markdown += `**${htmlToMarkdown(node).trim()}**`; break; case 'em': case 'i': markdown += `*${htmlToMarkdown(node).trim()}*`; break; case 'code': // 对于行内代码,保留内部空格但移除首尾空白 const inlineCode = node.textContent.replace(/^\s+|\s+$/g, ''); markdown += `\`${inlineCode}\``; break; case 'pre': // 检查是否包含code标签 const codeElement = node.querySelector('code'); if (codeElement) { // 尝试获取语言类型 const className = codeElement.className || ''; const langMatch = className.match(/language-(\w+)/); const lang = langMatch ? langMatch[1] : ''; // 获取代码内容,特别处理换行符 const codeText = extractCodeText(codeElement); markdown += `\n\`\`\`${lang}\n${codeText}\n\`\`\`\n\n`; } else { // 获取代码内容,特别处理换行符 const codeText = extractCodeText(node); markdown += `\n\`\`\`\n${codeText}\n\`\`\`\n\n`; } break; case 'table': markdown += processTable(node) + '\n\n'; break; case 'thead': case 'tbody': case 'tr': case 'th': case 'td': // 在processTable中处理 break; case 'blockquote': const quoteContent = htmlToMarkdown(node); markdown += `\n> ${quoteContent.trim().replace(/\n/g, '\n> ')}\n\n`; break; case 'ul': case 'ol': markdown += '\n'; const listItems = node.querySelectorAll('li'); listItems.forEach((li, index) => { const prefix = tagName === 'ul' ? '- ' : `${index + 1}. `; markdown += `${prefix}${htmlToMarkdown(li).trim()}\n`; }); markdown += '\n'; break; case 'li': // 在上面的ul/ol中处理 break; case 'hr': markdown += '\n---\n\n'; break; case 'del': case 's': markdown += `~~${htmlToMarkdown(node).trim()}~~`; break; case 'mark': markdown += `==${htmlToMarkdown(node).trim()}==`; break; case 'sub': markdown += `~${htmlToMarkdown(node).trim()}~`; break; case 'sup': markdown += `^${htmlToMarkdown(node).trim()}^`; break; case 'div': case 'span': case 'section': // 检查是否有特殊样式或类名 const className = node.className || ''; const style = node.style || {}; // 处理代码块容器 if (className.includes('code') || className.includes('highlight') || className.includes('prism') || className.includes('hljs')) { // 使用专门的函数处理代码内容,保留格式 const codeContent = extractCodeText(node); if (codeContent.trim()) { // 尝试从类名中提取语言信息 const langMatch = className.match(/(?:language|lang|hljs)-(\w+)/); const lang = langMatch ? langMatch[1] : ''; markdown += `\n\`\`\`${lang}\n${codeContent}\n\`\`\`\n\n`; } } // 处理引用框 - 更严格的判断条件,只有明确的引用样式才处理 else if (className.includes('blockquote') || (className.includes('quote') && (className.includes('block') || className.includes('box'))) || (style.borderLeft && style.borderLeft.includes('solid') && style.borderLeftWidth && parseInt(style.borderLeftWidth) >= 3)) { const quoteContent = htmlToMarkdown(node); if (quoteContent.trim()) { markdown += `\n> ${quoteContent.trim().replace(/\n/g, '\n> ')}\n\n`; } } // 检查是否包含图片 else { const images = node.querySelectorAll('img'); if (images.length > 0) { // 如果是纯图片容器(只包含一个img元素或主要是图片内容) const textContent = node.textContent.trim(); if ((node.children.length === 1 && node.children[0].tagName === 'IMG') || (images.length > 0 && textContent.length < 50)) { // 直接处理图片,避免重复 images.forEach(img => { const imageMarkdown = processImage(img); if (imageMarkdown) { markdown += imageMarkdown; } }); } else { // 包含图片的混合内容,递归处理所有子元素 markdown += htmlToMarkdown(node); } } else { // 不包含图片,正常递归处理子元素 markdown += htmlToMarkdown(node); } } break; default: // 对于其他标签,递归处理内容 markdown += htmlToMarkdown(node); break; } } } return markdown; } /** * 清理和格式化Markdown文本 * @param {string} markdown 原始Markdown文本 * @returns {string} 清理后的Markdown文本 */ function cleanMarkdown(markdown) { // 先分离代码块和行内代码,避免清理时影响代码内容 const codeBlocks = []; const inlineCodes = []; let codeBlockIndex = 0; let inlineCodeIndex = 0; // 提取代码块并用占位符替换 let processedMarkdown = markdown.replace(/```[\s\S]*?```/g, (match) => { const placeholder = `__CODE_BLOCK_${codeBlockIndex}__`; codeBlocks[codeBlockIndex] = match; codeBlockIndex++; return placeholder; }); // 提取行内代码并用占位符替换 processedMarkdown = processedMarkdown.replace(/`[^`\n]+`/g, (match) => { const placeholder = `__INLINE_CODE_${inlineCodeIndex}__`; inlineCodes[inlineCodeIndex] = match; inlineCodeIndex++; return placeholder; }); // 对非代码内容进行清理 processedMarkdown = processedMarkdown // 移除多余的空行(超过2个连续换行符) .replace(/\n{3,}/g, '\n\n') // 移除行首行尾的空格,但保留强制换行的两个空格 .replace(/^[ \t]+|(?<! )[ \t]+$/gm, '') // 清理多余的空格 .replace(/ {3,}/g, ' ') // 移除重复的图片链接(连续出现的相同图片链接) .replace(/(![\[^\]]*\]\([^)]+\))\s*\n\s*\1/g, '$1') // 移除空白的图片链接 .replace(/!\[\]\(\s*\)/g, '') .replace(/![\[^\]]*\]\(\s*\)/g, '') // 修复段落间距 .replace(/\n\n\n+/g, '\n\n') // 确保标题前后有适当的空行 .replace(/(\n|^)(#{1,6} .+)\n(?!\n)/g, '$1$2\n\n') // 移除文档开头的空行 .replace(/^\n+/g, '') // 优化文档结尾:移除多余空行,确保以双换行符结尾(标准Markdown格式) .replace(/\n+$/g, ''); // 恢复行内代码 for (let i = 0; i < inlineCodes.length; i++) { processedMarkdown = processedMarkdown.replace(`__INLINE_CODE_${i}__`, inlineCodes[i]); } // 恢复代码块 for (let i = 0; i < codeBlocks.length; i++) { processedMarkdown = processedMarkdown.replace(`__CODE_BLOCK_${i}__`, codeBlocks[i]); } return processedMarkdown + '\n\n'; } /** * 展开所有折叠的内容 * @param {Element} container 容器元素 */ function expandHiddenContent(container) { console.log('[微信转MD] 开始展开隐藏内容'); // 查找并点击所有可能的展开按钮 const expandButtons = container.querySelectorAll( '[data-action="expand"], .expand-btn, .show-more, .unfold, ' + '[onclick*="expand"], [onclick*="show"], [onclick*="unfold"], ' + 'button[class*="expand"], button[class*="more"], ' + 'a[class*="expand"], a[class*="more"], ' + '.js_unfold, .js_show_more, .js_expand' ); console.log('[微信转MD] 找到展开按钮数量:', expandButtons.length); expandButtons.forEach((btn, index) => { try { console.log(`[微信转MD] 点击展开按钮 ${index + 1}:`, btn.className, btn.textContent.trim()); btn.click(); } catch (e) { console.log(`[微信转MD] 点击按钮 ${index + 1} 失败:`, e.message); } }); // 查找并展开所有隐藏的div const hiddenElements = container.querySelectorAll( '[style*="display: none"], [style*="display:none"], ' + '.hidden, .collapse, .fold, .js_fold, ' + '[data-fold="true"], [data-hidden="true"]' ); console.log('[微信转MD] 找到隐藏元素数量:', hiddenElements.length); hiddenElements.forEach((el, index) => { try { console.log(`[微信转MD] 展开隐藏元素 ${index + 1}:`, el.className); el.style.display = 'block'; el.style.visibility = 'visible'; el.style.opacity = '1'; el.style.height = 'auto'; el.style.maxHeight = 'none'; // 移除隐藏相关的类名 el.classList.remove('hidden', 'collapse', 'fold', 'js_fold'); el.removeAttribute('data-fold'); el.removeAttribute('data-hidden'); } catch (e) { console.log(`[微信转MD] 展开元素 ${index + 1} 失败:`, e.message); } }); // 特别处理图片相关的折叠内容 const imageContainers = container.querySelectorAll('.img_loading, .img-box, .image-container, [data-src]'); imageContainers.forEach((imgContainer, index) => { try { // 查找图片容器内的隐藏内容 const hiddenInImg = imgContainer.querySelectorAll('[style*="display: none"], .hidden'); hiddenInImg.forEach(hidden => { hidden.style.display = 'block'; hidden.style.visibility = 'visible'; }); // 触发图片容器的点击事件 if (imgContainer.onclick || imgContainer.getAttribute('onclick')) { console.log(`[微信转MD] 触发图片容器 ${index + 1} 点击事件`); imgContainer.click(); } } catch (e) { console.log(`[微信转MD] 处理图片容器 ${index + 1} 失败:`, e.message); } }); console.log('[微信转MD] 隐藏内容展开完成'); } /** * 转换文章为Markdown格式 */ async function convertToMarkdown() { console.log('[微信转MD] 开始转换文章'); try { // 获取文章信息 const title = getArticleTitle(); const author = getArticleAuthor(); const publishTime = getPublishTime(); console.log('[微信转MD] 文章信息:', { title, author, publishTime }); // 获取文章内容 - 尝试多种选择器 const selectors = [ '#js_content', '.rich_media_content', '.article-content', '[data-role="content"]', '.rich_media_area_primary', '.rich_media_area_primary_inner', '.rich_media_wrp', '#page-content', '.page-content', '.content', '.post-content', '.article-body', '.entry-content', 'main', '[role="main"]' ]; let contentElement = null; let usedSelector = ''; // 尝试所有预定义的选择器 for (const selector of selectors) { const element = document.querySelector(selector); if (element && element.textContent.trim().length > 100) { contentElement = element; usedSelector = selector; console.log(`[微信转MD] 使用选择器 "${selector}" 找到内容,长度: ${element.textContent.length}`); break; } } // 如果还没找到,尝试查找包含文章内容的最大容器 if (!contentElement) { console.log('[微信转MD] 预定义选择器都未找到内容,尝试智能查找...'); const candidates = document.querySelectorAll('div, section, article, main'); let bestCandidate = null; let maxLength = 0; for (const candidate of candidates) { const textLength = candidate.textContent.trim().length; if (textLength > 500 && textLength > maxLength) { // 排除一些明显不是文章内容的元素 const className = candidate.className || ''; const id = candidate.id || ''; if (!className.includes('nav') && !className.includes('header') && !className.includes('footer') && !className.includes('sidebar') && !id.includes('nav') && !id.includes('header') && !id.includes('footer')) { bestCandidate = candidate; maxLength = textLength; } } } if (bestCandidate) { contentElement = bestCandidate; usedSelector = `智能查找: ${bestCandidate.tagName}.${bestCandidate.className || 'no-class'}`; console.log(`[微信转MD] 智能查找找到内容容器,长度: ${maxLength}`); } } // 如果仍然没找到,显示页面结构信息帮助调试 if (!contentElement) { console.log('[微信转MD] 未找到文章内容,分析页面结构...'); const allDivs = document.querySelectorAll('div'); console.log(`[微信转MD] 页面共有 ${allDivs.length} 个div元素`); // 显示文本长度最大的前5个元素 const elementsWithText = Array.from(document.querySelectorAll('*')) .filter(el => el.textContent.trim().length > 50) .sort((a, b) => b.textContent.length - a.textContent.length) .slice(0, 5); console.log('[微信转MD] 文本内容最多的前5个元素:'); elementsWithText.forEach((el, index) => { console.log(`${index + 1}. ${el.tagName}.${el.className || 'no-class'} (${el.textContent.length}字符)`); }); // 检查是否在验证页面 const pageText = document.body.textContent; if (pageText.includes('环境异常') || pageText.includes('验证') || pageText.includes('安全验证')) { console.log('[微信转MD] 检测到验证页面,请先完成人工验证'); alert('检测到微信安全验证页面,请先完成人工验证后再使用脚本。'); return; } } if (!contentElement) { console.log('[微信转MD] 未找到文章内容'); alert('未找到文章内容,请确保在微信公众号文章页面使用此脚本。'); return; } console.log('[微信转MD] 使用的内容选择器:', usedSelector); console.log('[微信转MD] 内容元素:', contentElement.tagName, contentElement.className || 'no-class'); console.log('[微信转MD] 内容长度:', contentElement.textContent.length); // 检查内容质量 const contentText = contentElement.textContent.trim(); const hasImages = contentElement.querySelectorAll('img').length; const hasParagraphs = contentElement.querySelectorAll('p').length; const hasHeadings = contentElement.querySelectorAll('h1, h2, h3, h4, h5, h6').length; console.log('[微信转MD] 内容质量分析:', { textLength: contentText.length, imageCount: hasImages, paragraphCount: hasParagraphs, headingCount: hasHeadings, preview: contentText.substring(0, 200) + (contentText.length > 200 ? '...' : '') }); // 展开所有隐藏的内容 expandHiddenContent(contentElement); // 等待一下让展开操作完成 await new Promise(resolve => setTimeout(resolve, 1000)); console.log('[微信转MD] 展开后内容长度:', contentElement.textContent.length); // 预处理图片元素 const allImages = contentElement.querySelectorAll('img'); console.log('[微信转MD] 找到图片数量:', allImages.length); // 分析图片属性 allImages.forEach((img, index) => { const attributes = {}; for (let attr of img.attributes) { attributes[attr.name] = attr.value; } console.log(`[微信转MD] 图片 ${index + 1} 属性:`, attributes); }); console.log('[微信转MD] 找到文章内容,开始转换'); // 分析文章内容结构 const allElements = contentElement.querySelectorAll('*'); const elementTypes = new Set(); allElements.forEach(el => elementTypes.add(el.tagName.toLowerCase())); console.log('[微信转MD] 文章包含的HTML元素类型:', Array.from(elementTypes).sort()); // 特别检查代码相关元素 const codeElements = contentElement.querySelectorAll('pre, code, .code, .highlight, .hljs, .prism'); console.log('[微信转MD] 找到代码相关元素数量:', codeElements.length); if (codeElements.length > 0) { codeElements.forEach((el, index) => { console.log(`[微信转MD] 代码元素 ${index + 1}:`, { tagName: el.tagName, className: el.className, textLength: el.textContent.length, preview: el.textContent.substring(0, 100) + (el.textContent.length > 100 ? '...' : '') }); }); } // 检查表格元素 const tableElements = contentElement.querySelectorAll('table'); console.log('[微信转MD] 找到表格数量:', tableElements.length); // 转换内容为Markdown let markdown = ''; // 添加文章头部信息 markdown += `# ${title}\n\n`; if (author) { markdown += `**作者:** ${author}\n\n`; } if (publishTime) { markdown += `**发布时间:** ${publishTime}\n\n`; } markdown += '---\n\n'; // 转换文章内容 const contentMarkdown = htmlToMarkdown(contentElement); markdown += contentMarkdown; // 清理格式 markdown = cleanMarkdown(markdown); console.log('[微信转MD] 转换完成,准备下载'); // 创建下载 downloadMarkdown(markdown, title); } catch (error) { console.error('[微信转MD] 转换过程中出现错误:', error); alert('转换失败,请查看控制台了解详细错误信息。'); } } /** * 下载Markdown文件 * @param {string} content Markdown内容 * @param {string} filename 文件名 */ function downloadMarkdown(content, filename) { // 清理文件名中的非法字符 const cleanFilename = filename.replace(/[<>:"/\\|?*]/g, '_'); // 创建Blob对象 const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }); // 创建下载链接 const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${cleanFilename}.md`; // 触发下载 document.body.appendChild(link); link.click(); document.body.removeChild(link); // 释放URL对象 URL.revokeObjectURL(url); // 显示成功消息 showSuccessMessage(`文章已成功转换并下载为:${cleanFilename}.md`); } /** * 显示成功消息 * @param {string} message 消息内容 */ function showSuccessMessage(message) { const messageDiv = document.createElement('div'); messageDiv.textContent = message; messageDiv.style.cssText = ` position: fixed; top: 80px; right: 20px; z-index: 10000; background: #4caf50; color: white; padding: 15px 20px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); font-size: 14px; max-width: 300px; word-wrap: break-word; `; document.body.appendChild(messageDiv); // 3秒后自动移除消息 setTimeout(() => { if (messageDiv.parentNode) { messageDiv.parentNode.removeChild(messageDiv); } }, 3000); } /** * 初始化脚本 */ function init() { // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createDownloadButton); } else { createDownloadButton(); } } // 启动脚本 init(); })();