您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键复制网页数学公式到Word/LaTeX | 智能悬停预览 | 支持维基百科/知乎/豆包/ChatGPT/stackexchange/DeepSeek等主流网站 | 自动识别并转换LaTeX/MathML格式 | 可视化反馈提示
- // ==UserScript==
- // @name LatexCopier
- // @name:zh-CN Latex公式复制助手 支持一键复制到Word文档/支持直接复制Latex代码(一键切换) 支持ChatGPT 维基百科 豆包 DeepSeek stackexchange 等主流网站
- // @namespace https://github.com/BakaDream/LatexCopier
- // @version 1.0.0
- // @license GPLv3
- // @description 一键复制网页数学公式到Word/LaTeX | 智能悬停预览 | 支持维基百科/知乎/豆包/ChatGPT/stackexchange/DeepSeek等主流网站 | 自动识别并转换LaTeX/MathML格式 | 可视化反馈提示
- // @author BakaDream
- // @match *://*.wikipedia.org/*
- // @match *://*.zhihu.com/*
- // @match *://*.chatgpt.com/*
- // @match *://*.stackexchange.com/*
- // @match *://*.doubao.com/*
- // @match *://*.deepseek.com/*
- // @require https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
- // @grant GM_registerMenuCommand
- // @grant GM_setValue
- // @grant GM_getValue
- // ==/UserScript==
- (function() {
- 'use strict';
- // ========================
- // 配置中心
- // ========================
- const CONFIG = {
- STORAGE_KEY: 'latexCopyMode',
- MODES: {
- WORD: {
- id: 'word',
- name: 'Word公式模式',
- desc: '生成MathML,粘贴到Word自动转为公式',
- feedback: '公式已复制 ✓ 可粘贴到Word'
- },
- RAW: {
- id: 'raw',
- name: '原始LaTeX模式',
- desc: '直接复制LaTeX源代码',
- feedback: 'LaTeX代码已复制 ✓'
- }
- },
- DEFAULT_MODE: 'word',
- SITE_TARGETS: {
- 'wikipedia.org': {
- selector: 'span.mwe-math-element',
- extractor: el => el.querySelector('math')?.getAttribute('alttext')
- },
- 'zhihu.com': {
- selector: 'span.ztext-math',
- extractor: el => el.getAttribute('data-tex')
- },
- 'doubao.com': {
- selector: 'span.math-inline',
- extractor: el => el.getAttribute('data-custom-copy-text')
- },
- 'chatgpt.com': {
- selector: 'span.katex',
- extractor: el => el.querySelector('annotation')?.textContent
- },
- 'stackexchange.com': {
- selector: 'span.math-container',
- extractor: el => el.querySelector('script')?.textContent
- },
- 'deepseek.com': {
- selector: 'span.katex',
- extractor: el => el.querySelector('annotation')?.textContent
- }
- }
- };
- // ========================
- // 工具模块
- // ========================
- const Utils = {
- getSiteConfig(url) {
- for (const [domain, config] of Object.entries(CONFIG.SITE_TARGETS)) {
- if (url.includes(domain)) return config;
- }
- return null;
- },
- copyToClipboard(text) {
- const textarea = document.createElement('textarea');
- textarea.style.position = 'fixed';
- textarea.style.left = '-9999px';
- textarea.style.opacity = 0;
- textarea.value = text;
- document.body.appendChild(textarea);
- textarea.select();
- let success = false;
- try {
- success = document.execCommand('copy');
- } catch (err) {
- console.error('[LaTeX助手] 复制失败:', err);
- } finally {
- document.body.removeChild(textarea);
- }
- return success;
- }
- };
- // ========================
- // 主功能模块
- // ========================
- const LaTeXCopyHelper = {
- currentMode: null,
- tooltip: null,
- feedback: null,
- activeElements: new Set(),
- // ===== 初始化 =====
- init() {
- this.currentMode = this._loadMode();
- this._initStyles(); // 合并样式配置和注入
- this._initUIElements();
- this._setupEventListeners();
- this._registerMenuCommand();
- },
- // ===== 模式管理 =====
- _loadMode() {
- const savedMode = GM_getValue(CONFIG.STORAGE_KEY);
- return Object.values(CONFIG.MODES).find(m => m.id === savedMode) || CONFIG.MODES.WORD;
- },
- _registerMenuCommand() {
- GM_registerMenuCommand(
- `切换模式 | 当前: ${this.currentMode.name}`,
- () => this._toggleMode()
- );
- },
- _toggleMode() {
- const newMode = this.currentMode === CONFIG.MODES.WORD
- ? CONFIG.MODES.RAW
- : CONFIG.MODES.WORD;
- GM_setValue(CONFIG.STORAGE_KEY, newMode.id);
- this.currentMode = newMode;
- this._showFeedback(`已切换为: ${newMode.name}\n${newMode.desc}`, true);
- setTimeout(() => location.reload(), 300);
- },
- // ===== UI管理 =====
- _initStyles() {
- const STYLES = {
- // 公式悬停效果
- HOVER: {
- background: 'rgba(100, 180, 255, 0.15)',
- boxShadow: '0 0 8px rgba(0, 120, 215, 0.3)',
- transition: 'all 0.3s ease'
- },
- // 工具提示
- TOOLTIP: {
- background: 'rgba(0, 0, 0, 0.85)',
- color: 'white',
- padding: '8px 12px',
- borderRadius: '4px',
- maxWidth: '400px',
- fontSize: '13px',
- offset: 10,
- arrowSize: '5px'
- },
- // 操作反馈
- FEEDBACK: {
- background: '#4CAF50',
- errorBackground: '#f44336',
- color: 'white',
- duration: 1500,
- position: 'bottom: 20%; left: 50%'
- }
- };
- const style = document.createElement('style');
- style.textContent = `
- /* 公式元素悬停效果 */
- [data-latex-copy] {
- cursor: pointer;
- transition: ${STYLES.HOVER.transition};
- border-radius: 3px;
- padding: 2px;
- position: relative;
- }
- [data-latex-copy]:hover {
- background: ${STYLES.HOVER.background} !important;
- box-shadow: ${STYLES.HOVER.boxShadow} !important;
- }
- /* 智能工具提示 */
- .latex-helper-tooltip {
- position: fixed;
- background: ${STYLES.TOOLTIP.background};
- color: ${STYLES.TOOLTIP.color};
- padding: ${STYLES.TOOLTIP.padding};
- border-radius: ${STYLES.TOOLTIP.borderRadius};
- max-width: min(${STYLES.TOOLTIP.maxWidth}, 90vw);
- font-size: ${STYLES.TOOLTIP.fontSize};
- z-index: 9999;
- opacity: 0;
- transform: translateY(5px);
- transition: all 0.2s ease;
- pointer-events: none;
- word-break: break-word;
- }
- .latex-helper-tooltip.visible {
- opacity: 1;
- transform: translateY(0);
- }
- .latex-helper-tooltip::after {
- content: '';
- position: absolute;
- left: 10px;
- border-width: ${STYLES.TOOLTIP.arrowSize};
- border-style: solid;
- border-color: transparent;
- }
- .latex-helper-tooltip.top-direction::after {
- bottom: calc(-${STYLES.TOOLTIP.arrowSize} * 2);
- border-top-color: ${STYLES.TOOLTIP.background};
- }
- .latex-helper-tooltip.bottom-direction::after {
- top: calc(-${STYLES.TOOLTIP.arrowSize} * 2);
- border-bottom-color: ${STYLES.TOOLTIP.background};
- }
- /* 操作反馈提示 */
- .latex-helper-feedback {
- position: fixed;
- ${STYLES.FEEDBACK.position};
- transform: translateX(-50%);
- background: ${STYLES.FEEDBACK.background};
- color: ${STYLES.FEEDBACK.color};
- padding: 10px 20px;
- border-radius: 4px;
- z-index: 10000;
- opacity: 0;
- transition: all 0.3s;
- text-align: center;
- white-space: pre-wrap;
- }
- .latex-helper-feedback.error {
- background: ${STYLES.FEEDBACK.errorBackground} !important;
- }
- .latex-helper-feedback.visible {
- opacity: 1;
- transform: translateX(-50%) translateY(-10px);
- }
- `;
- document.head.appendChild(style);
- },
- _initUIElements() {
- this.tooltip = document.createElement('div');
- this.tooltip.className = 'latex-helper-tooltip';
- document.body.appendChild(this.tooltip);
- this.feedback = document.createElement('div');
- this.feedback.className = 'latex-helper-feedback';
- document.body.appendChild(this.feedback);
- },
- // ===== 事件处理 =====
- _setupEventListeners() {
- document.addEventListener('mouseover', (e) => this._handleHover(e));
- document.addEventListener('mouseout', (e) => this._handleMouseOut(e));
- document.addEventListener('dblclick', (e) => this._handleDoubleClick(e), true);
- new MutationObserver((mutations) => this._handleMutations(mutations))
- .observe(document.body, { childList: true, subtree: true });
- },
- _handleHover(e) {
- const siteConfig = Utils.getSiteConfig(window.location.href);
- const element = e.target.closest(siteConfig?.selector || '');
- if (!element) return this._hideTooltip();
- element.setAttribute('data-latex-copy', 'true');
- this.activeElements.add(element);
- const latex = siteConfig.extractor(element);
- if (latex) this._showSmartTooltip(latex, element);
- },
- _handleMouseOut(e) {
- const element = e.target.closest('[data-latex-copy]');
- if (element) {
- element.style.background = '';
- element.style.boxShadow = '';
- }
- this._hideTooltip();
- },
- _handleDoubleClick(e) {
- const siteConfig = Utils.getSiteConfig(window.location.href);
- const element = e.target.closest(siteConfig?.selector || '');
- if (!element) return;
- const latex = siteConfig.extractor(element);
- if (!latex) return;
- this._copyFormula(latex);
- e.preventDefault();
- e.stopPropagation();
- },
- _handleMutations(mutations) {
- const siteConfig = Utils.getSiteConfig(window.location.href);
- if (!siteConfig) return;
- mutations.forEach((mutation) => {
- mutation.addedNodes.forEach((node) => {
- if (node.nodeType === 1 && node.matches(siteConfig.selector)) {
- node.setAttribute('data-latex-copy', 'true');
- this.activeElements.add(node);
- }
- });
- });
- },
- // ===== 核心功能 =====
- _showSmartTooltip(text, element) {
- this.tooltip.textContent = text;
- const rect = element.getBoundingClientRect();
- const tooltipHeight = this.tooltip.offsetHeight;
- const viewportHeight = window.innerHeight;
- // 优先显示在上方
- if (rect.top - tooltipHeight - 10 > 0) {
- this.tooltip.style.top = `${rect.top - tooltipHeight - 10}px`;
- this.tooltip.style.left = `${rect.left}px`;
- this.tooltip.className = 'latex-helper-tooltip top-direction visible';
- }
- // 上方空间不足时显示在下方
- else if (rect.bottom + tooltipHeight + 10 < viewportHeight) {
- this.tooltip.style.top = `${rect.bottom + 10}px`;
- this.tooltip.style.left = `${rect.left}px`;
- this.tooltip.className = 'latex-helper-tooltip bottom-direction visible';
- }
- // 极端情况:显示在元素旁边
- else {
- this.tooltip.style.top = `${rect.top}px`;
- this.tooltip.style.left = `${rect.right + 10}px`;
- this.tooltip.className = 'latex-helper-tooltip visible';
- }
- },
- _hideTooltip() {
- this.tooltip.className = 'latex-helper-tooltip';
- },
- async _copyFormula(latex) {
- if (this.currentMode === CONFIG.MODES.RAW) {
- this._copyRawLatex(latex);
- } else {
- await this._copyAsMathML(latex);
- }
- },
- _copyRawLatex(latex) {
- const success = Utils.copyToClipboard(latex);
- this._showFeedback(
- success ? this.currentMode.feedback : '复制失败 ✗',
- success
- );
- },
- async _copyAsMathML(latex) {
- try {
- MathJax.texReset();
- const mathML = await MathJax.tex2mmlPromise(latex);
- const success = Utils.copyToClipboard(mathML);
- this._showFeedback(
- success ? this.currentMode.feedback : '转换失败 ✗',
- success
- );
- } catch (error) {
- console.error('[LaTeX助手] MathJax转换错误:', error);
- this._showFeedback('公式转换失败,请尝试原始模式', false);
- }
- },
- _showFeedback(message, isSuccess) {
- this.feedback.textContent = message;
- this.feedback.className = `latex-helper-feedback ${isSuccess ? '' : 'error'} visible`;
- setTimeout(() => {
- this.feedback.className = 'latex-helper-feedback';
- }, 1500);
- }
- };
- // ========================
- // 启动脚本
- // ========================
- LaTeXCopyHelper.init();
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址