MRLookup

自动提取BibTeX数据并修改BibTeX关键字为AUTHOR_YEAR_TITLE的形式.

  1. // ==UserScript==
  2. // @name MRLookup
  3. // @namespace vanabeljs
  4. // @description Extract BibTeX data automatically and modify BibTeX Key to AUTHOR_YEAR_TITLE.
  5. // @description:ZH-CN 自动提取BibTeX数据并修改BibTeX关键字为AUTHOR_YEAR_TITLE的形式.
  6. // @copyright 2018, Van Abel (https://home.vanabel.cn)
  7. // @license OSI-SPDX-Short-Identifier
  8. // @version 3.0.4
  9. // @include */mathscinet/search/publications.html?fmt=bibtex*
  10. // @include */mathscinet/clipboard.html
  11. // @include */mrlookup
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_setClipboard
  16. // ==/UserScript==
  17.  
  18. // ==OpenUserJS==
  19. // @author Van Abel
  20. // ==/OpenUserJS==
  21.  
  22. /**
  23. * Webhook test: Testing automatic sync to Greasy Fork
  24. */
  25.  
  26. /*The first word to ignore in title*/
  27. var IgnoreStringInTitle = [
  28. 'a',
  29. 'an',
  30. 'on',
  31. 'the',
  32. 'another'
  33. ];
  34. function IgnoreStringToRegExp(arr) {
  35. var regexp = '^(';
  36. var arrlen = arr.length;
  37. for (var i = 0; i < arrlen; i++) {
  38. if (i == arrlen - 1) {
  39. regexp += '(' + arr[i] + ')';
  40. } else {
  41. regexp += '(' + arr[i] + ')|';
  42. }
  43. }
  44. regexp += ')\\s+';
  45. return regexp;
  46. }//console.log(IgnoreStringToRegExp(IgnoreStringInTitle));
  47. /*split bibdata*/
  48.  
  49. function parseBibTexLine(text) {
  50. try {
  51. var m = text.match(/^\s*(\S+)\s*=\s*/);
  52. if (!m) {
  53. console.error('Invalid line format:', text);
  54. return null;
  55. }
  56. var name = m[1];
  57. var search = text.slice(m[0].length);
  58. var re = /[\n\r,{}]/g;
  59. var braceCount = 0;
  60. var length = m[0].length;
  61. do {
  62. m = re.exec(search);
  63. if (!m) break;
  64. if (m[0] === '{') {
  65. braceCount++;
  66. } else if (m[0] === '}') {
  67. if (braceCount === 0) {
  68. throw new Error('Unexpected closing brace: "}"');
  69. }
  70. braceCount--;
  71. }
  72. } while (braceCount > 0);
  73. return {
  74. field: name,
  75. value: search.slice(0, re.lastIndex),
  76. length: length + re.lastIndex + (m ? m[0].length : 0)
  77. };
  78. } catch (error) {
  79. console.error('Error parsing BibTeX line:', error);
  80. return null;
  81. }
  82. }
  83. function parseBibTex(text) {
  84. var m = text.match(/^\s*@([^{]+){([^,\n]+)[,\n]/);
  85. if (!m) {
  86. throw new Error('Unrecogonised header format');
  87. }
  88. var result = {
  89. typeName: m[1].trim(),
  90. citationKey: m[2].trim()
  91. };
  92. text = text.slice(m[0].length).trim();
  93. while (text[0] !== '}') {
  94. var pair = parseBibTexLine(text);
  95. if (!pair) break;
  96. // Convert field name to lowercase for consistency
  97. result[pair.field.toLowerCase()] = pair.value;
  98. text = text.slice(pair.length).trim();
  99. }
  100. return result;
  101. }
  102.  
  103. function cleanAuthorName(author) {
  104. if (!author) return '';
  105. // 分割多个作者
  106. let authors = author.split(/\s*and\s*/);
  107. // 获取所有作者的姓
  108. let lastNames = authors.map(author => {
  109. // 提取姓(通常是逗号前的部分)
  110. let lastName = author.split(',')[0];
  111. // 对于复合姓氏,取最后一部分
  112. let nameParts = lastName.split(/\s+/);
  113. let finalLastName = nameParts[nameParts.length - 1];
  114. // 清理特殊字符
  115. return finalLastName.replace(/[{}\\\s\'"`]/g, '');
  116. });
  117. // 拼接所有作者的姓
  118. return lastNames.join('');
  119. }
  120.  
  121. function cleanTitle(title) {
  122. if (!title) return '';
  123. // 移除LaTeX命令和数学符号
  124. title = title.replace(/\\[a-zA-Z]+/g, '') // 移除LaTeX命令
  125. .replace(/\$[^$]*\$/g, '') // 移除数学公式
  126. .replace(/[{}\\\'"`]/g, '') // 移除特殊字符
  127. .replace(/\{[^}]*\}/g, ''); // 移除花括号内容
  128. // 按空格、连字符和标点符号分割成单词
  129. let words = title.split(/[\s\-,.:;]+/)
  130. .filter(word => word.length > 0) // 移除空字符串
  131. .map(word => word.replace(/[^a-zA-Z]/g, '')); // 只保留字母
  132. // 找到第一个有意义的单词
  133. for (let word of words) {
  134. // 转换为小写进行比较
  135. let lowercaseWord = word.toLowerCase();
  136. // 检查是否是忽略词或单个字母
  137. if (!IgnoreStringInTitle.includes(lowercaseWord) &&
  138. word.length > 1 &&
  139. !/^\d+$/.test(word)) { // 排除纯数字
  140. // 转换为首字母大写
  141. return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
  142. }
  143. }
  144. // 如果没有找到合适的单词,返回空字符串
  145. return '';
  146. }
  147.  
  148. // Configuration
  149. const CONFIG = {
  150. useJournal: GM_getValue('useJournal', true), // Default use journal abbreviation
  151. debug: GM_getValue('debug', false) // Debug mode
  152. };
  153.  
  154. // Test data for debug mode
  155. const DEFAULT_TEST_DATA = `@misc{chen2014l2modulispacessymplecticvortices,
  156. title={$L^2$-moduli spaces of symplectic vortices on Riemann surfaces with cylindrical ends},
  157. author={Bohui Chen and Bai-Ling Wang},
  158. year={2014},
  159. eprint={1405.6387},
  160. archivePrefix={arXiv},
  161. primaryClass={math.SG},
  162. url={https://arxiv.org/abs/1405.6387},
  163. }`;
  164.  
  165. // Get stored test data or use default
  166. let TEST_DATA = GM_getValue('testData', DEFAULT_TEST_DATA);
  167.  
  168. // Function to update test data
  169. function updateTestData(newData) {
  170. TEST_DATA = newData;
  171. GM_setValue('testData', newData);
  172. }
  173.  
  174. // 注册菜单命令
  175. GM_registerMenuCommand('Toggle Journal/Title Mode', function() {
  176. const currentMode = GM_getValue('useJournal', true);
  177. const newMode = !currentMode;
  178. GM_setValue('useJournal', newMode);
  179. CONFIG.useJournal = newMode;
  180. // 显示当前模式
  181. const modeText = newMode ? 'Journal Mode' : 'Title Mode';
  182. alert('Switched to ' + modeText);
  183. // 刷新页面以应用新设置
  184. location.reload();
  185. });
  186.  
  187. // Add status indicator
  188. function addStatusIndicator() {
  189. // Remove existing indicator if any
  190. const existingIndicator = document.getElementById('mode-indicator');
  191. if (existingIndicator) {
  192. existingIndicator.remove();
  193. }
  194.  
  195. const indicator = document.createElement('div');
  196. indicator.id = 'mode-indicator';
  197. indicator.style.cssText = `
  198. position: fixed;
  199. top: 10px;
  200. right: 10px;
  201. padding: 5px 10px;
  202. background: #f0f0f0;
  203. border: 1px solid #ccc;
  204. border-radius: 3px;
  205. font-size: 12px;
  206. z-index: 9999;
  207. cursor: pointer;
  208. user-select: none;
  209. margin-bottom: 5px;
  210. `;
  211. indicator.textContent = CONFIG.useJournal ? 'Mode: Journal' : 'Mode: Title';
  212. // Add click handler to toggle mode
  213. indicator.addEventListener('click', function() {
  214. const currentMode = GM_getValue('useJournal', true);
  215. const newMode = !currentMode;
  216. GM_setValue('useJournal', newMode);
  217. CONFIG.useJournal = newMode;
  218. // Update indicator text
  219. this.textContent = newMode ? 'Mode: Journal' : 'Mode: Title';
  220. // Update citation keys immediately
  221. updateBibTeXEntries();
  222. });
  223. document.body.appendChild(indicator);
  224. }
  225.  
  226. // Function to update all BibTeX entries
  227. function updateBibTeXEntries() {
  228. const els = document.getElementsByTagName('pre');
  229. for (let i = 0; i < els.length; i++) {
  230. try {
  231. const el = els[i];
  232. const bibdata = parseBibTex(el.innerHTML);
  233. if (!bibdata) continue;
  234.  
  235. // Extract author
  236. const author = cleanAuthorName(bibdata.author);
  237. // Extract year
  238. let year = '';
  239. if (bibdata.year) {
  240. let yearMatch = bibdata.year.match(/\d{4}/);
  241. if (yearMatch) {
  242. year = yearMatch[0];
  243. }
  244. }
  245. // Get identifier based on current mode
  246. let identifier = '';
  247. if (CONFIG.useJournal && bibdata.journal) {
  248. identifier = getJournalAbbrev(bibdata.journal);
  249. if (!identifier) {
  250. identifier = cleanTitle(bibdata.title);
  251. }
  252. } else {
  253. identifier = cleanTitle(bibdata.title);
  254. }
  255. // Create new BibTeX key
  256. const bibkey = author + year + identifier;
  257. // Replace the citation key in the original text
  258. const originalText = el.innerHTML;
  259. const newText = originalText.replace(/@([^{]+){([^,\n]+)[,\n]/, `@$1{${bibkey},`);
  260. // Update the content
  261. el.innerHTML = newText;
  262. // Add click to copy functionality if not already present
  263. if (!el.hasAttribute('data-click-handler')) {
  264. el.setAttribute('data-click-handler', 'true');
  265. el.addEventListener('click', function() {
  266. try {
  267. var bibdata_lb = this.innerHTML
  268. .replace(/\r|\n/g, '\r\n')
  269. .replace(/^\r\n/g, '')
  270. .replace(/\s*$/g, '\r\n')
  271. .replace(/\r\n\r\n/g, '\r\n');
  272. GM_setClipboard(bibdata_lb);
  273. } catch (error) {
  274. console.error('Error copying to clipboard:', error);
  275. }
  276. });
  277. }
  278. } catch (error) {
  279. console.error('Error updating BibTeX entry:', error);
  280. }
  281. }
  282. }
  283.  
  284. // 在页面加载完成后添加状态指示器
  285. window.addEventListener('load', function() {
  286. addStandardizeButton();
  287. addStatusIndicator();
  288. addDebugToggle();
  289. addTestDataButton();
  290. // Update all BibTeX entries on page load
  291. updateBibTeXEntries();
  292. });
  293.  
  294. // 添加新的期刊处理函数
  295. function getJournalAbbrev(journal) {
  296. if (!journal) return '';
  297. // 移除LaTeX命令和特殊字符
  298. journal = journal.replace(/\\[a-zA-Z]+/g, '') // 移除LaTeX命令
  299. .replace(/[{}\\\'"`]/g, '') // 移除特殊字符
  300. .replace(/\([^)]*\)/g, '') // 移除括号内容
  301. .replace(/\{[^}]*\}/g, '') // 移除花括号内容
  302. .trim();
  303. // 分割成单词
  304. let words = journal.split(/[\s.]+/).filter(word => word.length > 0);
  305. if (words.length === 1) {
  306. // 如果只有一个单词,取前三个字母并转为大写
  307. return words[0].slice(0, 3).toUpperCase();
  308. } else {
  309. // 多个单词时提取大写字母
  310. let abbrev = journal.match(/[A-Z]/g);
  311. return abbrev ? abbrev.join('') : '';
  312. }
  313. }
  314.  
  315. /**
  316. * auto set bibtex item checked for mrlookup site
  317. */
  318. var url = location.pathname;
  319. if (url.includes('mrlookup')) {
  320. document.getElementsByName('format')[1].checked = true;
  321. }
  322.  
  323. // Add button to standardize BibTeX
  324. function addStandardizeButton() {
  325. const button = document.createElement('button');
  326. button.textContent = 'Standardize BibTeX';
  327. button.style.cssText = `
  328. position: fixed;
  329. top: 45px;
  330. right: 10px;
  331. padding: 5px 10px;
  332. background: #4CAF50;
  333. color: white;
  334. border: none;
  335. border-radius: 3px;
  336. cursor: pointer;
  337. z-index: 9999;
  338. margin-bottom: 5px;
  339. `;
  340. button.addEventListener('click', standardizeBibTeX);
  341. document.body.appendChild(button);
  342. }
  343.  
  344. // Add test data button when debug is on
  345. function addTestDataButton() {
  346. // Remove existing test button if any
  347. const existingBtn = document.getElementById('test-data-btn');
  348. if (existingBtn) {
  349. existingBtn.remove();
  350. }
  351. // Only add button if debug mode is on
  352. if (!CONFIG.debug) return;
  353. const testBtn = document.createElement('button');
  354. testBtn.id = 'test-data-btn';
  355. testBtn.textContent = 'Test Data';
  356. testBtn.style.cssText = `
  357. position: fixed;
  358. top: 115px;
  359. right: 10px;
  360. padding: 5px 10px;
  361. background: #4CAF50;
  362. color: white;
  363. border: none;
  364. border-radius: 3px;
  365. cursor: pointer;
  366. z-index: 9999;
  367. margin-bottom: 5px;
  368. `;
  369. testBtn.addEventListener('click', () => {
  370. // First open the dialog
  371. standardizeBibTeX();
  372. // Then fill it with test data
  373. setTimeout(() => {
  374. const textarea = document.querySelector('textarea');
  375. if (textarea) {
  376. textarea.value = TEST_DATA;
  377. }
  378. }, 100); // Small delay to ensure dialog is created
  379. });
  380. document.body.appendChild(testBtn);
  381. }
  382.  
  383. // Add debug mode toggle
  384. function addDebugToggle() {
  385. const debugBtn = document.createElement('button');
  386. debugBtn.textContent = CONFIG.debug ? 'Debug: ON' : 'Debug: OFF';
  387. debugBtn.style.cssText = `
  388. position: fixed;
  389. top: 80px;
  390. right: 10px;
  391. padding: 5px 10px;
  392. background: ${CONFIG.debug ? '#ff4444' : '#f0f0f0'};
  393. color: ${CONFIG.debug ? 'white' : 'black'};
  394. border: 1px solid #ccc;
  395. border-radius: 3px;
  396. cursor: pointer;
  397. z-index: 9999;
  398. margin-bottom: 5px;
  399. `;
  400. debugBtn.addEventListener('click', function() {
  401. CONFIG.debug = !CONFIG.debug;
  402. GM_setValue('debug', CONFIG.debug);
  403. this.textContent = CONFIG.debug ? 'Debug: ON' : 'Debug: OFF';
  404. this.style.background = CONFIG.debug ? '#ff4444' : '#f0f0f0';
  405. this.style.color = CONFIG.debug ? 'white' : 'black';
  406. // Update test data button visibility
  407. addTestDataButton();
  408. });
  409. document.body.appendChild(debugBtn);
  410. }
  411.  
  412. // Show debug result
  413. function showDebugResult(result) {
  414. if (!CONFIG.debug) return;
  415. // Remove existing debug result if any
  416. const existingResult = document.getElementById('debug-result');
  417. if (existingResult) {
  418. existingResult.remove();
  419. }
  420. // Create overlay
  421. const overlay = document.createElement('div');
  422. overlay.style.cssText = `
  423. position: fixed;
  424. top: 0;
  425. left: 0;
  426. right: 0;
  427. bottom: 0;
  428. background: rgba(0,0,0,0.5);
  429. z-index: 10000;
  430. `;
  431. const resultDiv = document.createElement('div');
  432. resultDiv.id = 'debug-result';
  433. resultDiv.style.cssText = `
  434. position: fixed;
  435. top: 50%;
  436. left: 50%;
  437. transform: translate(-50%, -50%);
  438. background: white;
  439. padding: 20px;
  440. border-radius: 5px;
  441. box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  442. z-index: 10001;
  443. width: 80%;
  444. max-width: 800px;
  445. max-height: 80vh;
  446. overflow: auto;
  447. `;
  448. const pre = document.createElement('pre');
  449. pre.style.cssText = `
  450. white-space: pre-wrap;
  451. word-wrap: break-word;
  452. font-family: monospace;
  453. font-size: 14px;
  454. margin: 0;
  455. padding: 10px;
  456. background: #f8f8f8;
  457. border-radius: 3px;
  458. `;
  459. pre.textContent = result;
  460. const closeBtn = document.createElement('button');
  461. closeBtn.textContent = 'Close';
  462. closeBtn.style.cssText = `
  463. position: absolute;
  464. top: 10px;
  465. right: 10px;
  466. padding: 5px 15px;
  467. background: #f0f0f0;
  468. border: 1px solid #ccc;
  469. border-radius: 3px;
  470. cursor: pointer;
  471. `;
  472. closeBtn.onclick = () => {
  473. document.body.removeChild(overlay);
  474. };
  475. // Close when clicking outside
  476. overlay.addEventListener('click', (event) => {
  477. if (event.target === overlay) {
  478. document.body.removeChild(overlay);
  479. }
  480. });
  481. resultDiv.appendChild(closeBtn);
  482. resultDiv.appendChild(pre);
  483. overlay.appendChild(resultDiv);
  484. document.body.appendChild(overlay);
  485. }
  486.  
  487. // Modify standardizeBibTeX to handle debug mode
  488. function standardizeBibTeX() {
  489. // Create dialog container
  490. const dialog = document.createElement('div');
  491. dialog.style.cssText = ` position: fixed;
  492. top: 50%;
  493. left: 50%;
  494. transform: translate(-50%, -50%);
  495. background: white;
  496. padding: 20px;
  497. border-radius: 5px;
  498. box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  499. z-index: 10000;
  500. width: 80%;
  501. max-width: 800px;
  502. `;
  503.  
  504. // Create textarea
  505. const textarea = document.createElement('textarea');
  506. textarea.style.cssText = `
  507. width: 100%;
  508. height: 200px;
  509. margin: 10px 0;
  510. padding: 10px;
  511. border: 1px solid #ccc;
  512. border-radius: 3px;
  513. font-family: monospace;
  514. font-size: 14px;
  515. resize: vertical;
  516. `;
  517. textarea.placeholder = 'Paste your BibTeX entry here...';
  518.  
  519. // Create buttons container
  520. const buttons = document.createElement('div');
  521. buttons.style.cssText = `
  522. text-align: right;
  523. margin-top: 10px;
  524. `;
  525.  
  526. // Create cancel button
  527. const cancelBtn = document.createElement('button');
  528. cancelBtn.textContent = 'Cancel';
  529. cancelBtn.style.cssText = `
  530. padding: 5px 15px;
  531. margin-right: 10px;
  532. background: #f0f0f0;
  533. border: 1px solid #ccc;
  534. border-radius: 3px;
  535. cursor: pointer;
  536. `;
  537.  
  538. // Create submit button
  539. const submitBtn = document.createElement('button');
  540. submitBtn.textContent = 'Standardize';
  541. submitBtn.style.cssText = `
  542. padding: 5px 15px;
  543. background: #4CAF50;
  544. color: white;
  545. border: none;
  546. border-radius: 3px;
  547. cursor: pointer;
  548. `;
  549.  
  550. // Create overlay
  551. const overlay = document.createElement('div');
  552. overlay.style.cssText = `
  553. position: fixed;
  554. top: 0;
  555. left: 0;
  556. right: 0;
  557. bottom: 0;
  558. background: rgba(0,0,0,0.5);
  559. z-index: 9999;
  560. `;
  561.  
  562. // Add event listeners
  563. cancelBtn.onclick = () => {
  564. document.body.removeChild(overlay);
  565. };
  566.  
  567. // Close dialog when clicking outside
  568. overlay.addEventListener('click', (event) => {
  569. // Only close if clicking directly on the overlay, not its children
  570. if (event.target === overlay) {
  571. document.body.removeChild(overlay);
  572. }
  573. });
  574.  
  575. submitBtn.onclick = () => {
  576. const input = textarea.value.trim();
  577. if (!input) {
  578. alert('Please enter a BibTeX entry');
  579. return;
  580. }
  581.  
  582. try {
  583. // Store the input as test data if in debug mode
  584. if (CONFIG.debug) {
  585. updateTestData(input);
  586. }
  587.  
  588. // Parse the input BibTeX
  589. const bibdata = parseBibTex(input);
  590. if (!bibdata) {
  591. alert('Invalid BibTeX format');
  592. return;
  593. }
  594.  
  595. // Clean up the data by removing extra braces and preserving math
  596. const cleanValue = (value) => {
  597. if (!value) return '';
  598. // Remove only the outermost braces if they exist
  599. // This preserves all LaTeX commands and math formulas
  600. return value.replace(/^{|}$/g, '');
  601. };
  602.  
  603. // Extract author
  604. const author = cleanAuthorName(cleanValue(bibdata.author));
  605. // Extract year
  606. let year = '';
  607. if (bibdata.year) {
  608. let yearMatch = cleanValue(bibdata.year).match(/\d{4}/);
  609. if (yearMatch) {
  610. year = yearMatch[0];
  611. }
  612. }
  613. // Get identifier based on current mode
  614. let identifier = '';
  615. if (CONFIG.useJournal && bibdata.journal) {
  616. identifier = getJournalAbbrev(cleanValue(bibdata.journal));
  617. if (!identifier) {
  618. identifier = cleanTitle(cleanValue(bibdata.title));
  619. }
  620. } else {
  621. identifier = cleanTitle(cleanValue(bibdata.title));
  622. }
  623. // Create new BibTeX key
  624. const bibkey = author + year + identifier;
  625.  
  626. // Get all field names from the input, preserving their original case
  627. const fieldNames = Object.keys(bibdata).filter(key =>
  628. key !== 'typeName' && key !== 'citationKey'
  629. ).map(key => key.toUpperCase());
  630.  
  631. // Find the longest field name for alignment
  632. const maxLength = Math.max(...fieldNames.map(name => name.length));
  633.  
  634. // Function to format a field with proper alignment
  635. const formatField = (name, value) => {
  636. const padding = ' '.repeat(maxLength - name.length);
  637. // Clean the value and ensure it's properly wrapped in braces
  638. const cleanedValue = cleanValue(value);
  639. return ` ${name}${padding} = {${cleanedValue}},\n`;
  640. };
  641.  
  642. // Standardize the format
  643. let standardized = `@${bibdata.typeName} {${bibkey},\n`;
  644. // Add all fields from the input
  645. for (const field of fieldNames) {
  646. const value = bibdata[field.toLowerCase()];
  647. if (value) {
  648. standardized += formatField(field, value);
  649. }
  650. }
  651.  
  652. // Remove trailing comma and add closing brace
  653. standardized = standardized.replace(/,\n$/, '\n}');
  654.  
  655. if (CONFIG.debug) {
  656. // Show debug result
  657. showDebugResult(standardized);
  658. } else {
  659. // Copy to clipboard
  660. GM_setClipboard(standardized);
  661. alert('Standardized BibTeX has been copied to clipboard!');
  662. }
  663. document.body.removeChild(overlay);
  664. } catch (error) {
  665. console.error('Error standardizing BibTeX:', error);
  666. alert('Error standardizing BibTeX. Please check the console for details.');
  667. }
  668. };
  669.  
  670. // Assemble dialog
  671. buttons.appendChild(cancelBtn);
  672. buttons.appendChild(submitBtn);
  673. dialog.appendChild(textarea);
  674. dialog.appendChild(buttons);
  675. overlay.appendChild(dialog);
  676. document.body.appendChild(overlay);
  677.  
  678. // Focus textarea
  679. textarea.focus();
  680. }
  681.