TexCopyer

双击网页中的LaTex公式,将其复制到剪切板

  1. // ==UserScript==
  2. // @name TexCopyer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @license GPLv3
  6. // @description 双击网页中的LaTex公式,将其复制到剪切板
  7. // @description:en Double click on a LaTeX formula on a webpage to copy it to the clipboard
  8. // @author yjy
  9. // @match *://*.wikipedia.org/*
  10. // @match *://*.zhihu.com/*
  11. // @match *://*.chatgpt.com/*
  12. // @match *://*.moonshot.cn/*
  13. // @match *://*.stackexchange.com/*
  14. // @match *://oi-wiki.org/*
  15. // @match *://*.luogu.com/*
  16. // @match *://*.luogu.com.cn/*
  17. // @match *://*.doubao.com/*
  18. // @match *://*.deepseek.com/*
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23. // 插入样式表
  24. const css = `
  25. .latex-tooltip { position: fixed; background-color: rgba(0, 0, 0, 0.7); color: #fff; padding: 5px 10px; border-radius: 5px; font-size: 11px; z-index: 1000; opacity: 0; transition: opacity 0.2s; pointer-events: none; }
  26. .latex-copy-success { position: fixed; bottom: 10%; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.7); color: #fff; padding: 10px 20px; border-radius: 5px; font-size: 12px; z-index: 1000; transition: opacity 0.2s; pointer-events: none; }
  27. `;
  28. const styleSheet = document.createElement("style");
  29. styleSheet.type = "text/css";
  30. styleSheet.innerText = css;
  31. document.head.appendChild(styleSheet);
  32.  
  33. // 创建提示框元素
  34. const tooltip = document.createElement('div');
  35. tooltip.classList.add('latex-tooltip');
  36. document.body.appendChild(tooltip);
  37.  
  38. // 获取对象和公式方法
  39. function getTarget(url) {
  40. let target = { elementSelector: '', getLatexString: null }
  41. // 格式化latex
  42. function formatLatex(input) {
  43. while (input.endsWith(' ') || input.endsWith('\\')) {
  44. input = input.slice(0, -1);
  45. }
  46. return '$' + input + '$';
  47. }
  48. if (url.includes('wikipedia.org')) {
  49. target.elementSelector = 'span.mwe-math-element';
  50. target.getLatexString = (element) => formatLatex(element.querySelector('math').getAttribute('alttext'));
  51. return target
  52.  
  53. } else if (url.includes('zhihu.com')) {
  54. target.elementSelector = 'span.ztext-math';
  55. target.getLatexString = (element) => formatLatex(element.getAttribute('data-tex'));
  56. return target
  57.  
  58. } else if (url.includes('chatgpt.com')) {
  59. target.elementSelector = 'span.katex';
  60. target.getLatexString = (element) => formatLatex(element.querySelector('annotation').textContent);
  61. return target
  62.  
  63. } else if (url.includes('moonshot.cn')) {
  64. target.elementSelector = 'span.katex';
  65. target.getLatexString = (element) => formatLatex(element.querySelector('annotation').textContent);
  66. return target
  67. } else if (url.includes('stackexchange.com')) {
  68. target.elementSelector = 'span.math-container';
  69. target.getLatexString = (element) => formatLatex(element.querySelector('script').textContent);
  70. return target
  71. }
  72. else if (url.includes('oi-wiki.org')) {
  73. target.elementSelector = 'mjx-container.MathJax';
  74. target.getLatexString = (element) => formatLatex(element.querySelector('img').title);
  75. return target
  76. }
  77. else if (url.includes('luogu.com')) {
  78. target.elementSelector = 'span.katex';
  79. target.getLatexString = (element) => formatLatex(element.querySelector('annotation').textContent);
  80. return target
  81. }
  82. else if (url.includes('doubao.com')) {
  83. target.elementSelector = 'span.math-inline';
  84. target.getLatexString = (element) => formatLatex(element.getAttribute('data-custom-copy-text'));
  85. return target
  86. }
  87. else if (url.includes('deepseek.com')) {
  88. target.elementSelector = 'span.katex';
  89. target.getLatexString = (element) => formatLatex(element.querySelector('annotation').textContent);
  90. return target
  91. }
  92. // 待添加更多网站的条件
  93. return null;
  94. }
  95.  
  96. // 绑定事件到元素
  97. function addHandler() {
  98. let target = getTarget(window.location.href);
  99. if (!target) return;
  100.  
  101. let tooltipTimeout;
  102. document.querySelectorAll(target.elementSelector).forEach(element => {
  103. element.addEventListener('mouseenter', function () {
  104. element.style.cursor = "pointer";
  105. tooltipTimeout = setTimeout(function () {
  106. tooltip.textContent = getTarget(window.location.href).getLatexString(element);;
  107. const rect = element.getBoundingClientRect();
  108. tooltip.style.left = `${rect.left}px`;
  109. tooltip.style.top = `${rect.top - tooltip.offsetHeight - 5}px`;
  110. tooltip.style.display = 'block';
  111. tooltip.style.opacity = '0.8';
  112. }, 1000); // 进入延迟1秒
  113. });
  114.  
  115. element.addEventListener('mouseleave', function () {
  116. element.style.cursor = "auto";
  117. clearTimeout(tooltipTimeout);
  118. tooltip.style.display = 'none';
  119. tooltip.style.opacity = '0';
  120. });
  121.  
  122. element.ondblclick = function () {
  123. const latexString = target.getLatexString(element)
  124. if (latexString !== null) {
  125. console.log(`LaTeX copied: ${latexString}`) // for debug
  126. navigator.clipboard.writeText(latexString).then(() => {
  127. showCopySuccessTooltip();
  128. });
  129. }
  130. // 取消网页上的选中状态(不是很优雅)
  131. window.getSelection().removeAllRanges();
  132. };
  133. });
  134. }
  135.  
  136. // 显示复制成功提示
  137. function showCopySuccessTooltip() {
  138. const copyTooltip = document.createElement("div");
  139. copyTooltip.className = "latex-copy-success";
  140. copyTooltip.innerText = "已复制LaTeX公式";
  141. document.body.appendChild(copyTooltip);
  142. setTimeout(() => {
  143. copyTooltip.style.opacity = "0";
  144. setTimeout(() => {
  145. document.body.removeChild(copyTooltip);
  146. }, 200); // 与transition时间匹配
  147. }, 1000); // 提示短时间后消失
  148. }
  149.  
  150. // 监听页面加载或变化,绑定事件
  151. document.addEventListener('DOMContentLoaded', addHandler);
  152. new MutationObserver(addHandler).observe(document.documentElement, { childList: true, subtree: true });
  153.  
  154.  
  155. })();