NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name 知乎复制公式到word
// @namespace http://*.zhihu.com
// @version 2.0
// @description 一键复制知乎文章或回答为Markdown格式,自动转换LaTeX公式,适配2026年新版知乎。
// @author mikecoding
// @match https://*.zhihu.com/*
// @copyright 2021, mikecoding (https://openuserjs.org/users/mikecoding)
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @require https://unpkg.com/turndown@7.1.1/dist/turndown.js
// @license MIT
// @grant GM_setClipboard
// ==/UserScript==
(function() {
'use strict';
// HTML to Markdown 转换器
let turndownService = null;
// 初始化 Turndown
function initTurndown() {
if (typeof TurndownService === 'undefined') {
console.warn('TurndownService 未加载,使用简化版本');
return null;
}
turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
emDelimiter: '*',
strongDelimiter: '**',
bulletListMarker: '-'
});
// 自定义规则:处理知乎公式
turndownService.addRule('zhihuFormula', {
filter: function(node) {
return node.classList && (
node.classList.contains('ztext-math') ||
node.hasAttribute('data-tex')
);
},
replacement: function(content, node) {
const tex = node.getAttribute('data-tex');
if (!tex) return content;
// 判断是行内还是行间公式
const isDisplay = tex.includes('\\begin{') ||
tex.includes('aligned') ||
tex.includes('\\\\') ||
tex.length > 60;
if (isDisplay) {
return '\n\n$$\n' + tex + '\n$$\n\n';
} else {
return ' $' + tex + '$ ';
}
}
});
// 处理图片
turndownService.addRule('images', {
filter: 'img',
replacement: function(content, node) {
// 如果是公式图片,跳过(已由上面的规则处理)
if (node.hasAttribute('data-formula') || node.classList.contains('ztext-math')) {
const tex = node.getAttribute('data-formula');
if (tex) {
return ' $' + tex + '$ ';
}
}
// 提取图片信息
const alt = node.getAttribute('alt') ||
node.getAttribute('data-caption') ||
'图片';
// 优先使用原图URL
const src = node.getAttribute('data-original') ||
node.getAttribute('src') ||
node.getAttribute('data-actualsrc') ||
'';
const title = node.getAttribute('title') || '';
if (!src) return '';
const titlePart = title ? ' "' + title + '"' : '';
return '\n\n\n\n';
}
});
// 处理链接
turndownService.addRule('links', {
filter: 'a',
replacement: function(content, node) {
const href = node.getAttribute('href') || '';
const title = node.getAttribute('title') || '';
if (!href) return content;
// 处理知乎内部链接
let fullHref = href;
if (href.startsWith('//')) {
fullHref = 'https:' + href;
} else if (href.startsWith('/')) {
fullHref = 'https://www.zhihu.com' + href;
}
const titlePart = title ? ' "' + title + '"' : '';
return '[' + content + '](' + fullHref + titlePart + ')';
}
});
return turndownService;
}
// 修复公式中的转义字符(完全版)
function unescapeFormulas(markdown) {
// 处理行间公式 $$...$$
markdown = markdown.replace(/\$\$([\s\S]*?)\$\$/g, function(match, formula) {
// 第一步:用占位符保护 \\\\ (四个反斜杠,代表 LaTeX 的 \\)
let unescaped = formula.replace(/\\\\\\\\/g, '<<<LATEX_NEWLINE>>>');
// 第二步:替换 \\ (两个反斜杠) 为 \ (一个反斜杠)
// 这会修复 \\begin -> \begin, \\text -> \text 等
unescaped = unescaped.replace(/\\\\/g, '\\');
// 第三步:恢复 LaTeX 换行符
unescaped = unescaped.replace(/<<<LATEX_NEWLINE>>>/g, '\\\\');
// 第四步:修复其他转义字符
unescaped = unescaped
.replace(/\\_/g, '_') // 下划线
.replace(/\\\*/g, '*') // 星号
.replace(/\\\[/g, '[') // 左方括号
.replace(/\\\]/g, ']') // 右方括号
.replace(/\\\{/g, '{') // 左花括号
.replace(/\\\}/g, '}') // 右花括号
.replace(/\\\(/g, '(') // 左圆括号
.replace(/\\\)/g, ')') // 右圆括号
.replace(/\\\|/g, '|') // 竖线
.replace(/\\>/g, '>') // 大于号
.replace(/\\</g, '<') // 小于号
.replace(/\\&/g, '&') // &符号
.replace(/\\\$/g, '$'); // 美元符号
return '$$' + unescaped + '$$';
});
// 处理行内公式 $...$
markdown = markdown.replace(/\$([^\$\n]+?)\$/g, function(match, formula) {
// 行内公式一般不会有 \\ 换行符,直接处理
let unescaped = formula
.replace(/\\\\/g, '\\') // 双反斜杠 -> 单反斜杠
.replace(/\\_/g, '_')
.replace(/\\\*/g, '*')
.replace(/\\\[/g, '[')
.replace(/\\\]/g, ']')
.replace(/\\\{/g, '{')
.replace(/\\\}/g, '}')
.replace(/\\\(/g, '(')
.replace(/\\\)/g, ')')
.replace(/\\\|/g, '|')
.replace(/\\>/g, '>')
.replace(/\\</g, '<')
.replace(/\\&/g, '&');
return '$' + unescaped + '$';
});
return markdown;
}
// 简化版 HTML to Markdown 转换(备用)
function simpleHtmlToMarkdown(html) {
let markdown = html;
// 处理图片(在处理其他标签之前)
markdown = markdown.replace(
/<img[^>]*>/gi,
function(match) {
// 提取属性
const srcMatch = match.match(/data-original="([^"]*)"|src="([^"]*)"/);
const altMatch = match.match(/alt="([^"]*)"|data-caption="([^"]*)"/);
const src = (srcMatch && (srcMatch[1] || srcMatch[2])) || '';
const alt = (altMatch && (altMatch[1] || altMatch[2])) || '图片';
if (!src) return '';
return '\n\n\n\n';
}
);
// 处理标题
markdown = markdown.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '\n# $1\n\n');
markdown = markdown.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '\n## $1\n\n');
markdown = markdown.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '\n### $1\n\n');
markdown = markdown.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '\n#### $1\n\n');
// 处理段落
markdown = markdown.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n');
// 处理粗体和斜体
markdown = markdown.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**');
markdown = markdown.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**');
markdown = markdown.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*');
markdown = markdown.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*');
// 处理链接
markdown = markdown.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)');
// 处理列表
markdown = markdown.replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n');
markdown = markdown.replace(/<ul[^>]*>(.*?)<\/ul>/gi, '\n$1\n');
markdown = markdown.replace(/<ol[^>]*>(.*?)<\/ol>/gi, '\n$1\n');
// 处理代码
markdown = markdown.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`');
markdown = markdown.replace(/<pre[^>]*>(.*?)<\/pre>/gi, '\n```\n$1\n```\n');
// 处理换行
markdown = markdown.replace(/<br[^>]*>/gi, '\n');
// 移除所有剩余的 HTML 标签
markdown = markdown.replace(/<[^>]+>/g, '');
// 清理多余的空行
markdown = markdown.replace(/\n{3,}/g, '\n\n');
return markdown.trim();
}
// 转换知乎公式为 Markdown
function convertFormulas(element) {
const clonedElement = element.cloneNode(true);
// 处理所有公式元素
clonedElement.querySelectorAll('[data-tex]').forEach(node => {
const tex = node.getAttribute('data-tex');
if (!tex) return;
// 判断是否为行间公式
const isDisplay = tex.includes('\\begin{') ||
tex.includes('aligned') ||
tex.includes('\\\\') ||
tex.length > 60;
const markdownFormula = isDisplay ?
`\n\n$$\n${tex}\n$$\n\n` :
` $${tex}$ `;
const textNode = document.createTextNode(markdownFormula);
node.parentNode.replaceChild(textNode, node);
});
// 处理图片中的公式(备用方法)
clonedElement.querySelectorAll('img[data-formula]').forEach(node => {
const tex = node.getAttribute('data-formula');
if (!tex) return;
const isDisplay = tex.includes('\\begin{') || tex.length > 60;
const markdownFormula = isDisplay ?
`\n\n$$\n${tex}\n$$\n\n` :
` $${tex}$ `;
const textNode = document.createTextNode(markdownFormula);
node.parentNode.replaceChild(textNode, node);
});
return clonedElement;
}
// 提取文章内容
function extractArticleContent() {
// 尝试找到文章主体
const contentSelectors = [
'.Post-RichText', // 文章
'.RichText.ztext', // 回答
'.Post-Main .RichContent',
'article .RichText',
'[itemprop="text"]',
];
let contentElement = null;
for (const selector of contentSelectors) {
contentElement = document.querySelector(selector);
if (contentElement) break;
}
if (!contentElement) {
showToast('未找到文章内容', 'error');
return null;
}
return contentElement;
}
// 提取文章标题
function extractTitle() {
const titleSelectors = [
'.Post-Title',
'.QuestionHeader-title',
'h1.Post-Title',
'[itemprop="name"]',
];
for (const selector of titleSelectors) {
const titleElement = document.querySelector(selector);
if (titleElement) {
return titleElement.textContent.trim();
}
}
return '知乎文章';
}
// 标记公式(点击复制)
let isHighlighted = false;
function highlightFormulas() {
const formulas = document.querySelectorAll('[data-tex]');
if (formulas.length === 0) {
showToast('未找到公式', 'error');
return;
}
if (!isHighlighted) {
formulas.forEach(formula => {
formula.style.backgroundColor = '#fff59d';
formula.style.cursor = 'pointer';
formula.style.padding = '2px 4px';
formula.style.borderRadius = '3px';
formula.onclick = function() {
const tex = this.getAttribute('data-tex');
if (tex) {
const isDisplay = tex.includes('\\begin{') || tex.length > 60;
const markdownFormula = isDisplay ?
`$$\n${tex}\n$$` :
`$${tex}$`;
copyToClipboard(markdownFormula);
showToast('已复制公式', 'success');
}
};
});
isHighlighted = true;
showToast(`已标记 ${formulas.length} 个公式(点击可复制)`, 'success');
} else {
formulas.forEach(formula => {
formula.style.backgroundColor = '';
formula.style.cursor = '';
formula.style.padding = '';
formula.onclick = null;
});
isHighlighted = false;
showToast('已取消标记', 'success');
}
}
// 复制所有公式
function copyAllFormulas() {
const formulas = document.querySelectorAll('[data-tex]');
if (formulas.length === 0) {
showToast('未找到公式', 'error');
return;
}
const formulaSet = new Set();
const formulaList = [];
formulas.forEach(formula => {
const tex = formula.getAttribute('data-tex');
if (tex && !formulaSet.has(tex)) {
formulaSet.add(tex);
const isDisplay = tex.includes('\\begin{') ||
tex.includes('aligned') ||
tex.includes('\\\\') ||
tex.length > 60;
const markdownFormula = isDisplay ?
`$$\n${tex}\n$$` :
`$${tex}$`;
formulaList.push(markdownFormula);
}
});
const allFormulas = formulaList.join('\n\n');
copyToClipboard(allFormulas);
showToast(`已复制 ${formulaList.length} 个公式`, 'success');
}
// 复制为 Markdown
function copyAsMarkdown() {
const contentElement = extractArticleContent();
if (!contentElement) return;
const title = extractTitle();
const processedContent = convertFormulas(contentElement);
let markdown;
if (turndownService) {
try {
markdown = turndownService.turndown(processedContent.innerHTML);
// 修复公式转义问题(完全版)
markdown = unescapeFormulas(markdown);
} catch (e) {
console.warn('Turndown 转换失败,使用简化版本', e);
markdown = simpleHtmlToMarkdown(processedContent.innerHTML);
}
} else {
markdown = simpleHtmlToMarkdown(processedContent.innerHTML);
}
// 添加标题
const fullMarkdown = `# ${title}\n\n${markdown}`;
// 复制到剪贴板
copyToClipboard(fullMarkdown);
showToast(`已复制 "${title}" 为 Markdown 格式`, 'success');
}
// 复制纯文本(含公式)
function copyAsText() {
const contentElement = extractArticleContent();
if (!contentElement) return;
const title = extractTitle();
const processedContent = convertFormulas(contentElement);
// 提取纯文本
let text = processedContent.textContent.trim();
// 清理多余空白
text = text.replace(/\n{3,}/g, '\n\n');
text = text.replace(/ +/g, ' ');
const fullText = `${title}\n\n${'='.repeat(title.length)}\n\n${text}`;
copyToClipboard(fullText);
showToast(`已复制 "${title}" 为纯文本格式`, 'success');
}
// 复制到剪贴板
function copyToClipboard(text) {
if (typeof GM_setClipboard !== 'undefined') {
GM_setClipboard(text);
} else if (navigator.clipboard) {
navigator.clipboard.writeText(text).catch(() => {
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
// 备用复制方法
function fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
// 显示提示
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.textContent = message;
const bgColor = type === 'success' ? '#10B981' : '#EF4444';
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${bgColor};
color: white;
padding: 12px 20px;
border-radius: 8px;
z-index: 10000;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
max-width: 400px;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// 添加控制面板
function addControlPanel() {
const panel = document.createElement('div');
panel.id = 'zhihu-markdown-panel';
panel.style.cssText = `
position: fixed;
bottom: 80px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
background: white;
padding: 15px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
`;
// 标题
const panelTitle = document.createElement('div');
panelTitle.textContent = '📝 知乎转换工具';
panelTitle.style.cssText = `
font-weight: bold;
font-size: 14px;
color: #333;
margin-bottom: 5px;
`;
panel.appendChild(panelTitle);
// 文章转换区域
const articleSection = document.createElement('div');
articleSection.style.cssText = `
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 10px;
`;
const articleLabel = document.createElement('div');
articleLabel.textContent = '文章转换';
articleLabel.style.cssText = `
font-size: 12px;
color: #666;
margin-bottom: 8px;
`;
articleSection.appendChild(articleLabel);
// 复制为 Markdown 按钮
const markdownBtn = createButton('📋 复制为 Markdown', '#0084ff', copyAsMarkdown);
articleSection.appendChild(markdownBtn);
// 复制为纯文本按钮
const textBtn = createButton('📄 复制为纯文本', '#00a600', copyAsText);
articleSection.appendChild(textBtn);
panel.appendChild(articleSection);
// 公式操作区域
const formulaSection = document.createElement('div');
const formulaLabel = document.createElement('div');
formulaLabel.textContent = '公式操作';
formulaLabel.style.cssText = `
font-size: 12px;
color: #666;
margin-bottom: 8px;
`;
formulaSection.appendChild(formulaLabel);
// 标记公式按钮
const highlightBtn = createButton('📐 标记公式', '#ff9800', highlightFormulas);
formulaSection.appendChild(highlightBtn);
// 复制所有公式按钮
const copyAllBtn = createButton('📑 复制所有公式', '#9c27b0', copyAllFormulas);
formulaSection.appendChild(copyAllBtn);
panel.appendChild(formulaSection);
// 折叠/展开按钮
const toggleBtn = document.createElement('div');
toggleBtn.textContent = '−';
toggleBtn.style.cssText = `
position: absolute;
top: 5px;
right: 10px;
cursor: pointer;
font-size: 20px;
color: #666;
user-select: none;
`;
toggleBtn.onclick = function() {
const isCollapsed = panel.style.height === '40px';
if (isCollapsed) {
panel.style.height = 'auto';
toggleBtn.textContent = '−';
} else {
panel.style.height = '40px';
panel.style.overflow = 'hidden';
toggleBtn.textContent = '+';
}
};
panel.appendChild(toggleBtn);
document.body.appendChild(panel);
}
// 创建按钮
function createButton(text, color, onClick) {
const btn = document.createElement('button');
btn.textContent = text;
btn.style.cssText = `
background: ${color};
color: white;
border: none;
padding: 8px 12px;
margin-top: 5px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
white-space: nowrap;
width: 100%;
`;
btn.onmouseover = function() {
btn.style.opacity = '0.9';
btn.style.transform = 'translateY(-1px)';
};
btn.onmouseout = function() {
btn.style.opacity = '1';
btn.style.transform = 'translateY(0)';
};
btn.onclick = onClick;
return btn;
}
// 初始化
function init() {
// 延迟执行,确保页面加载完成
setTimeout(() => {
initTurndown();
addControlPanel();
console.log('知乎文章/回答转Markdown工具已加载(v4.0.2 完全修复版)');
}, 1500);
}
// 页面加载完成后执行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();