Copy LaTeX Formula 1.1

复制网页上的 LaTeX 公式(CSDN,zhihu,wiki)

  1. // ==UserScript==
  2. // @name Copy LaTeX Formula 1.1
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description 复制网页上的 LaTeX 公式(CSDN,zhihu,wiki)
  6. // @author shezhao
  7. // @match *://*/*
  8. // @match https://www.zhihu.com/question/*
  9. // @match https://zhuanlan.zhihu.com/p/*
  10. // @match https://blog.csdn.net/*/article/*
  11. // @match https://*.wikipedia.org/*
  12. // @match https://www.wikiwand.com/*
  13. // @license MIT
  14. // @grant none
  15. // ==/UserScript==
  16. // 鸣谢
  17. // https://greasyfork.org/zh-CN/scripts/397740
  18. // 参考了wiki部分 https://github.com/flaribbit/click-to-copy-equations
  19. (function() {
  20. 'use strict';
  21. const host = document.location.host;
  22. class zhihuLaTeXFormulaCopier {
  23. constructor(elementSelector = 'span.ztext-math') {
  24. this.elementSelector = elementSelector;
  25. this.contextMenu = this.createContextMenu();
  26. this.addEventListeners();
  27. }
  28.  
  29. createContextMenu() {
  30. const contextMenu = document.createElement('div');
  31. contextMenu.style.display = 'none';
  32. contextMenu.style.position = 'absolute';
  33. contextMenu.style.backgroundColor = 'white';
  34. contextMenu.style.border = '1px solid black';
  35. contextMenu.style.padding = '5px';
  36. contextMenu.style.zIndex = '10000';
  37.  
  38. const copyOption = document.createElement('div');
  39. copyOption.textContent = 'Copy LaTeX Formula';
  40. copyOption.style.cursor = 'pointer';
  41. copyOption.style.padding = '5px';
  42. copyOption.addEventListener('click', () => {
  43. const formula = this.getSelectedFormula();
  44. if (formula) {
  45. console.log('Formula to be copied:', formula);
  46. this.copyToClipboard(formula);
  47. } else {
  48. console.log('No formula found');
  49. }
  50. this.hideContextMenu();
  51. });
  52.  
  53. contextMenu.appendChild(copyOption);
  54. document.body.appendChild(contextMenu);
  55.  
  56. return contextMenu;
  57. }
  58.  
  59. showContextMenu(x, y) {
  60. if (this.contextMenu) {
  61. this.contextMenu.style.left = `${x}px`;
  62. this.contextMenu.style.top = `${y}px`;
  63. this.contextMenu.style.display = 'block';
  64. console.log('Context menu shown at:', x, y);
  65. }
  66. }
  67.  
  68. hideContextMenu() {
  69. if (this.contextMenu) {
  70. this.contextMenu.style.display = 'none';
  71. console.log('Context menu hidden');
  72. }
  73. }
  74.  
  75. getSelectedFormula() {
  76. const selection = window.getSelection();
  77. if (selection.rangeCount > 0) {
  78. const range = selection.getRangeAt(0);
  79. const startNode = range.startContainer;
  80. const endNode = range.endContainer;
  81.  
  82. const latexElements = document.querySelectorAll(this.elementSelector);
  83. console.log('Found latex elements:', latexElements.length);
  84. for (const element of latexElements) {
  85. if (element.contains(startNode) && element.contains(endNode)) {
  86. const formula = element.getAttribute('data-tex');
  87. console.log('Selected formula:', formula);
  88. return '$' + formula + '$'; // 在这里添加 $ 符号
  89. }
  90. }
  91. }
  92. console.log('No formula selected');
  93. return null;
  94. }
  95.  
  96. copyToClipboard(text) {
  97. navigator.clipboard.writeText(text)
  98. .then(() => {
  99. console.log('Copied to clipboard:', text);
  100. if (!DEFAULT_COPY){
  101. alert('已复制到剪贴板');
  102. }
  103. })
  104. .catch((error) => {
  105. console.error('Failed to copy to clipboard:', error);
  106. if (!DEFAULT_COPY){
  107. alert('复制失败');
  108. }
  109. });
  110. }
  111.  
  112. addEventListeners() {
  113. document.addEventListener('contextmenu', (event) => {
  114. event.preventDefault();
  115. const clickedElement = event.target;
  116. this.highlightElement(clickedElement);
  117. const latexElement = this.findClosestLatexElement(clickedElement);
  118. if (latexElement) {
  119.  
  120. const formula = latexElement.getAttribute('data-tex');
  121. if (formula) {
  122. console.log('Formula found in clicked element:', formula);
  123. if (!DEFAULT_COPY) {
  124. let shouldCopy = window.confirm('是否要复制这个公式?');
  125. if (shouldCopy) {
  126. this.copyToClipboard('$' + formula + '$'); // 在这里添加 $ 符号
  127. }
  128. } else{
  129. this.copyToClipboard('$' + formula + '$'); // 在这里添加 $ 符号
  130. }
  131.  
  132. } else {
  133. console.log('No formula found in clicked element');
  134.  
  135. }
  136.  
  137. } else {
  138. console.log('No ztext-math element found in clicked area');
  139. }
  140. this.showContextMenu(event.pageX, event.pageY);
  141. });
  142.  
  143.  
  144. document.addEventListener('click', () => {
  145. this.hideContextMenu();
  146. this.removeHighlight();
  147. });
  148. }
  149.  
  150. findClosestLatexElement(element) {
  151. let currentElement = element;
  152. while (currentElement) {
  153. if (currentElement.classList && currentElement.classList.contains('ztext-math')) {
  154. return currentElement;
  155. }
  156. currentElement = currentElement.parentElement;
  157. }
  158. return null;
  159. }
  160. highlightElement(element) {
  161. this.removeHighlight();
  162. element.style.border = '2px solid red';
  163. this.highlightedElement = element;
  164. }
  165.  
  166. removeHighlight() {
  167. if (this.highlightedElement) {
  168. this.highlightedElement.style.border = '';
  169. this.highlightedElement = null;
  170. }
  171. }
  172. }
  173. class csdnKatexFormulaCopier {
  174. constructor(elementSelector = 'span.katex-mathml') {
  175. this.elementSelector = elementSelector;
  176. this.contextMenu = this.createContextMenu();
  177. this.addEventListeners();
  178.  
  179. }
  180.  
  181. createContextMenu() {
  182. const contextMenu = document.createElement('div');
  183. contextMenu.style.display = 'none';
  184. contextMenu.style.position = 'absolute';
  185. contextMenu.style.backgroundColor = 'white';
  186. contextMenu.style.border = '1px solid black';
  187. contextMenu.style.padding = '5px';
  188. contextMenu.style.zIndex = '10000';
  189.  
  190. const copyOption = document.createElement('div');
  191. copyOption.textContent = 'Copy LaTeX Formula';
  192. copyOption.style.cursor = 'pointer';
  193. copyOption.style.padding = '5px';
  194. copyOption.addEventListener('click', () => {
  195. const formula = this.getSelectedFormula();
  196. if (formula) {
  197. console.log('Formula to be copied:', formula);
  198. this.copyToClipboard(formula);
  199. } else {
  200. console.log('No formula found');
  201. }
  202. this.hideContextMenu();
  203. });
  204.  
  205. contextMenu.appendChild(copyOption);
  206. document.body.appendChild(contextMenu);
  207.  
  208. return contextMenu;
  209. }
  210.  
  211. showContextMenu(x, y) {
  212. if (this.contextMenu) {
  213. this.contextMenu.style.left = `${x}px`;
  214. this.contextMenu.style.top = `${y}px`;
  215. this.contextMenu.style.display = 'block';
  216. console.log('Context menu shown at:', x, y);
  217. }
  218. }
  219.  
  220. hideContextMenu() {
  221. if (this.contextMenu) {
  222. this.contextMenu.style.display = 'none';
  223. console.log('Context menu hidden');
  224. }
  225. }
  226.  
  227. getSelectedFormula() {
  228. const selection = window.getSelection();
  229. if (selection.rangeCount > 0) {
  230. const range = selection.getRangeAt(0);
  231. const startNode = range.startContainer;
  232. const endNode = range.endContainer;
  233.  
  234. const katexElements = document.querySelectorAll(this.elementSelector);
  235. console.log('Found katex elements:', katexElements.length);
  236. for (const element of katexElements) {
  237. if (element.contains(startNode) && element.contains(endNode)) {
  238. const formula = element.textContent;
  239. // 处理公式 以换行符分隔,获取最后一个公式
  240. const formulas = formula.split('\n');
  241. formula = formulas[formulas.length - 1];
  242. if (!formula) {
  243. if (formulas.length < 2) {
  244. console.log('No formula found');
  245. return null;
  246. }
  247. formula = formulas[formulas.length - 2];
  248. }
  249. console.log('Selected formula:', formula);
  250. return '$' + formula + '$';
  251. }
  252. }
  253. }
  254. console.log('No formula selected');
  255. return null;
  256. }
  257.  
  258. copyToClipboard(text) {
  259. navigator.clipboard.writeText(text)
  260. .then(() => {
  261. console.log('Copied to clipboard:', text);
  262. if (!DEFAULT_COPY) {
  263. alert('已复制到剪贴板');
  264. }
  265.  
  266.  
  267. })
  268. .catch((error) => {
  269. console.error('Failed to copy to clipboard:', error);
  270. if (!DEFAULT_COPY) {
  271. alert('复制失败');
  272. }
  273. });
  274. }
  275.  
  276. addEventListeners() {
  277. document.addEventListener('contextmenu', (event) => {
  278. event.preventDefault();
  279. const clickedElement = event.target;
  280. const katexElement = this.findClosestKatexElement(clickedElement);
  281.  
  282. if (katexElement) {
  283.  
  284. const formula = katexElement.textContent;
  285.  
  286. if (formula) {
  287. console.log(typeof formula);
  288. // 将公式按换行符分割
  289. const formulas_origin = formula.split("\n");
  290. let formula_text = "";
  291. let maxLength = 0;
  292.  
  293. for (let i = 0; i < formulas_origin.length; i++) {
  294. // 修剪每个公式的首尾空白
  295. const trimmedFormula = formulas_origin[i].trim();
  296. // 检查修剪后的公式是否不为空且长度大于当前最大长度
  297. if (trimmedFormula.length > 0 && trimmedFormula.length > maxLength) {
  298. formula_text = trimmedFormula;
  299. maxLength = trimmedFormula.length;
  300. }
  301. }
  302.  
  303. console.log('在点击的元素中找到的公式:', formula_text);
  304. if(!DEFAULT_COPY) {
  305. let shouldCopy = window.confirm('是否要复制这个公式?');
  306. if (shouldCopy) {
  307. this.copyToClipboard('$' + formula_text + '$');
  308. }
  309. }
  310. else {
  311. this.copyToClipboard('$' + formula_text + '$');
  312. }
  313. } else {
  314. console.log('No formula found in clicked element');
  315. }
  316. } else {
  317. console.log('No katex-mathml element found in clicked area');
  318. }
  319. this.showContextMenu(event.pageX, event.pageY);
  320. });
  321.  
  322. document.addEventListener('click', () => {
  323. this.hideContextMenu();
  324. });
  325. }
  326.  
  327. findClosestKatexElement(element) {
  328. let currentElement = element;
  329. while (currentElement) {
  330. if (currentElement.classList && currentElement.classList.contains('katex-mathml')) {
  331. return currentElement;
  332. }
  333. // 检查父元素的同级元素
  334. let sibling = currentElement.previousElementSibling;
  335. while (sibling) {
  336. if (sibling.classList && sibling.classList.contains('katex-mathml')) {
  337. return sibling;
  338. }
  339. sibling = sibling.previousElementSibling;
  340. }
  341. sibling = currentElement.nextElementSibling;
  342. while (sibling) {
  343. if (sibling.classList && sibling.classList.contains('katex-mathml')) {
  344. return sibling;
  345. }
  346. sibling = sibling.nextElementSibling;
  347. }
  348. currentElement = currentElement.parentElement;
  349. }
  350. return null;
  351. }
  352.  
  353. }
  354. // 用于复制维基百科和 Wiki 上的公式
  355. class WikiTeXFormulaCopier {
  356. constructor() {
  357. this.init();
  358. }
  359.  
  360. init() {
  361. if (host.search('wikipedia') >= 0) {
  362. this.setupWikipedia();
  363. } else if (host.search('wikiwand') >= 0) {
  364. this.setupWikiwand();
  365. }
  366. }
  367.  
  368. clearAnimation(event) {
  369. event.target.style.animation = '';
  370. }
  371.  
  372. setupWikipedia() {
  373. const copyTex = function () {
  374. if(!DEFAULT_COPY) {
  375. if (confirm('是否复制该公式?')) {
  376. navigator.clipboard.writeText('$' + this.alt + '$');
  377. this.style.animation = 'aniclick .2s';
  378. }
  379. }
  380. else {
  381. navigator.clipboard.writeText('$' + this.alt + '$');
  382. this.style.animation = 'aniclick .2s';
  383. }
  384.  
  385. }
  386. const eqs = document.querySelectorAll('.mwe-math-fallback-image-inline, .mwe-math-fallback-image-display');
  387. for (let i = 0; i < eqs.length; i++) {
  388. eqs[i].onclick = copyTex;
  389. eqs[i].addEventListener('animationend', this.clearAnimation);
  390. eqs[i].title = '点击即可复制公式';
  391. }
  392. }
  393.  
  394. setupWikiwand() {
  395. const copyTex = function () {
  396. const tex = this.getElementsByTagName('math')[0].getAttribute("alttext");
  397. if(!DEFAULT_COPY) {
  398. if (confirm('是否复制该公式?')) {
  399. navigator.clipboard.writeText('$' + tex + '$');
  400. this.style.animation = 'aniclick .2s';
  401. }
  402. }
  403. else {
  404. navigator.clipboard.writeText('$' + tex + '$');
  405. this.style.animation = 'aniclick .2s';
  406. }
  407.  
  408. }
  409. const check_equations = (mutationList, observer) => {
  410. const eqs = document.querySelectorAll('.mwe-math-element');
  411. for (let i = 0; i < eqs.length; i++) {
  412. eqs[i].onclick = copyTex;
  413. eqs[i].addEventListener('animationend', this.clearAnimation);
  414. eqs[i].title = '点击即可复制公式';
  415. }
  416. }
  417. const targetNode = document.getElementsByTagName('article')[0];
  418. const config = { attributes: false, childList: true, subtree: true };
  419. const observer = new MutationObserver(check_equations);
  420. observer.observe(targetNode, config);
  421. }
  422. }
  423. const DEFAULT_COPY = true
  424.  
  425. if (host.search('zhihu.com') >= 0||host.search('blog.csdn') >= 0||host.search('wikipedia') >= 0||host.search('wikiwand') >= 0)
  426. {
  427. // 默认复制到剪贴板
  428. const DEFAULT_COPY = window.confirm('是否默认复制到剪贴板?');
  429. // 网址包含 zhihu.com 的页面
  430. if (host.search('zhihu.com') >= 0) {
  431. new zhihuLaTeXFormulaCopier();
  432. }
  433. // 网址包含 csdn.net 的页面
  434. else if (host.search('blog.csdn') >= 0) {
  435. new csdnKatexFormulaCopier();
  436. }
  437. else if (host.search('wikipedia') >= 0 || host.search('wikiwand') >= 0) {
  438. new WikiTeXFormulaCopier();
  439. }
  440. }
  441.  
  442.  
  443. })();