// ==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();
})();