Greasy Fork 支持简体中文。

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 | blime
  9. // @match *://*.wikipedia.org/*
  10. // @match *://*.zhihu.com/*
  11. // @match *://*.chatgpt.com/*
  12. // @match *://*.moonshot.cn/*
  13. // @match *://*.stackexchange.com/*
  14. // @match *://*.kexue.fm/*
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19. // 插入样式表
  20. const css = `
  21. .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; }
  22. .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; }
  23. `;
  24. const styleSheet = document.createElement("style");
  25. styleSheet.type = "text/css";
  26. styleSheet.innerText = css;
  27. document.head.appendChild(styleSheet);
  28.  
  29. // 创建提示框元素
  30. const tooltip = document.createElement('div');
  31. tooltip.classList.add('latex-tooltip');
  32. document.body.appendChild(tooltip);
  33.  
  34. // 获取对象和公式方法
  35. function getTarget(url) {
  36. let target = {elementSelector: '', getLatexString: null}
  37. // 格式化latex
  38. function formatLatex(input) {
  39. while (input.endsWith(' ') || input.endsWith('\\')) {
  40. input = input.slice(0, -1);
  41. }
  42. return '$' + input + '$';
  43. }
  44. if (url.includes('wikipedia.org')) {
  45. target.elementSelector = 'span.mwe-math-element';
  46. target.getLatexString = (element) => formatLatex(element.querySelector('math').getAttribute('alttext'));
  47. return target
  48.  
  49. } else if (url.includes('zhihu.com')) {
  50. target.elementSelector = 'span.ztext-math';
  51. target.getLatexString = (element) => formatLatex(element.getAttribute('data-tex'));
  52. return target
  53.  
  54. } else if (url.includes('chatgpt.com')) {
  55. target.elementSelector = 'span.katex';
  56. target.getLatexString = (element) => formatLatex(element.querySelector('annotation').textContent);
  57. return target
  58. } else if (url.includes('moonshot.cn')) {
  59. target.elementSelector = 'span.katex';
  60. target.getLatexString = (element) => formatLatex(element.querySelector('annotation').textContent);
  61. return target
  62. } else if (url.includes('stackexchange.com')) {
  63. target.elementSelector = 'span.math-container';
  64. target.getLatexString = (element) => formatLatex(element.querySelector('script').textContent);
  65. return target
  66. } else if (url.includes('kexue.fm')) {
  67. target.elementSelector = 'math';
  68. target.getLatexString = (element) => formatLatex(element.querySelector('annotation[encoding="application/x-tex"]').textContent);
  69. return target;
  70. }
  71. // 待添加更多网站的条件
  72. return null;
  73. }
  74.  
  75. // 绑定事件到元素
  76. function addHandler() {
  77. let target = getTarget(window.location.href);
  78. if (!target) return;
  79.  
  80. let tooltipTimeout;
  81. document.querySelectorAll(target.elementSelector).forEach(element => {
  82. element.addEventListener('mouseenter', function () {
  83. element.style.cursor = "pointer";
  84. tooltipTimeout = setTimeout(function () {
  85. tooltip.textContent = getTarget(window.location.href).getLatexString(element);;
  86. const rect = element.getBoundingClientRect();
  87. tooltip.style.left = `${rect.left}px`;
  88. tooltip.style.top = `${rect.top - tooltip.offsetHeight - 5}px`;
  89. tooltip.style.display = 'block';
  90. tooltip.style.opacity = '0.8';
  91. }, 1000); // 进入延迟1秒
  92. });
  93.  
  94. element.addEventListener('mouseleave', function () {
  95. element.style.cursor = "auto";
  96. clearTimeout(tooltipTimeout);
  97. tooltip.style.display = 'none';
  98. tooltip.style.opacity = '0';
  99. });
  100. element.ondblclick = function() {
  101. const latexString = target.getLatexString(element)
  102. if (latexString !== null) {
  103. console.log(`LaTeX copied: ${latexString}`) // for debug
  104. navigator.clipboard.writeText(latexString).then(() => {
  105. showCopySuccessTooltip();
  106. });
  107. }
  108. // 取消网页上的选中状态(不是很优雅)
  109. window.getSelection().removeAllRanges();
  110. };
  111. });
  112. }
  113. // 显示复制成功提示
  114. function showCopySuccessTooltip() {
  115. const copyTooltip = document.createElement("div");
  116. copyTooltip.className = "latex-copy-success";
  117. copyTooltip.innerText = "已复制LaTeX公式";
  118. document.body.appendChild(copyTooltip);
  119. setTimeout(() => {
  120. copyTooltip.style.opacity = "0";
  121. setTimeout(() => {
  122. document.body.removeChild(copyTooltip);
  123. }, 200); // 与transition时间匹配
  124. }, 1000); // 提示短时间后消失
  125. }
  126.  
  127. // 监听页面加载或变化,绑定事件
  128. document.addEventListener('DOMContentLoaded', addHandler);
  129. new MutationObserver(addHandler).observe(document.documentElement, {childList: true, subtree: true});
  130.  
  131.  
  132. })();