MRLookup

Extract BibTeX data automatically and modify BibTeX Key to AUTHOR_YEAR_TITLE.

// ==UserScript==
// @name              MRLookup
// @namespace         vanabeljs
// @description       Extract BibTeX data automatically and modify BibTeX Key to AUTHOR_YEAR_TITLE.
// @description:ZH-CN 自动提取BibTeX数据并修改BibTeX关键字为AUTHOR_YEAR_TITLE的形式.
// @copyright         2018, Van Abel (https://home.vanabel.cn)
// @license           OSI-SPDX-Short-Identifier
// @version           3.0.4
// @include           */mathscinet/search/publications.html?fmt=bibtex*
// @include           */mathscinet/clipboard.html
// @include           */mrlookup
// @grant            GM_setValue
// @grant            GM_getValue
// @grant            GM_registerMenuCommand
// @grant            GM_setClipboard
// ==/UserScript==

// ==OpenUserJS==
// @author Van Abel
// ==/OpenUserJS==

/**
 * Webhook test: Testing automatic sync to Greasy Fork
 */

/*The first word to ignore in title*/
var IgnoreStringInTitle = [
	'a',
	'an',
	'on',
	'the',
	'another'
];
function IgnoreStringToRegExp(arr) {
	var regexp = '^(';
	var arrlen = arr.length;
	for (var i = 0; i < arrlen; i++) {
		if (i == arrlen - 1) {
			regexp += '(' + arr[i] + ')';
		} else {
			regexp += '(' + arr[i] + ')|';
		}
	}
	regexp += ')\\s+';
	return regexp;
}//console.log(IgnoreStringToRegExp(IgnoreStringInTitle));
/*split bibdata*/

function parseBibTexLine(text) {
	try {
		var m = text.match(/^\s*(\S+)\s*=\s*/);
		if (!m) {
			console.error('Invalid line format:', text);
			return null;
		}
		var name = m[1];
		var search = text.slice(m[0].length);
		var re = /[\n\r,{}]/g;
		var braceCount = 0;
		var length = m[0].length;
		do {
			m = re.exec(search);
			if (!m) break;
			if (m[0] === '{') {
				braceCount++;
			} else if (m[0] === '}') {
				if (braceCount === 0) {
					throw new Error('Unexpected closing brace: "}"');
				}
				braceCount--;
			}
		} while (braceCount > 0);
		return {
			field: name,
			value: search.slice(0, re.lastIndex),
			length: length + re.lastIndex + (m ? m[0].length : 0)
		};
	} catch (error) {
		console.error('Error parsing BibTeX line:', error);
		return null;
	}
}
function parseBibTex(text) {
	var m = text.match(/^\s*@([^{]+){([^,\n]+)[,\n]/);
	if (!m) {
		throw new Error('Unrecogonised header format');
	}
	var result = {
		typeName: m[1].trim(),
		citationKey: m[2].trim()
	};
	text = text.slice(m[0].length).trim();
	while (text[0] !== '}') {
		var pair = parseBibTexLine(text);
		if (!pair) break;
		// Convert field name to lowercase for consistency
		result[pair.field.toLowerCase()] = pair.value;
		text = text.slice(pair.length).trim();
	}
	return result;
}

function cleanAuthorName(author) {
	if (!author) return '';
	
	// 分割多个作者
	let authors = author.split(/\s*and\s*/);
	// 获取所有作者的姓
	let lastNames = authors.map(author => {
		// 提取姓(通常是逗号前的部分)
		let lastName = author.split(',')[0];
		// 对于复合姓氏,取最后一部分
		let nameParts = lastName.split(/\s+/);
		let finalLastName = nameParts[nameParts.length - 1];
		// 清理特殊字符
		return finalLastName.replace(/[{}\\\s\'"`]/g, '');
	});
	// 拼接所有作者的姓
	return lastNames.join('');
}

function cleanTitle(title) {
	if (!title) return '';
	
	// 移除LaTeX命令和数学符号
	title = title.replace(/\\[a-zA-Z]+/g, '')          // 移除LaTeX命令
		  .replace(/\$[^$]*\$/g, '')                   // 移除数学公式
		  .replace(/[{}\\\'"`]/g, '')                  // 移除特殊字符
		  .replace(/\{[^}]*\}/g, '');                  // 移除花括号内容
	
	// 按空格、连字符和标点符号分割成单词
	let words = title.split(/[\s\-,.:;]+/)
		.filter(word => word.length > 0)               // 移除空字符串
		.map(word => word.replace(/[^a-zA-Z]/g, '')); // 只保留字母
	
	// 找到第一个有意义的单词
	for (let word of words) {
		// 转换为小写进行比较
		let lowercaseWord = word.toLowerCase();
		// 检查是否是忽略词或单个字母
		if (!IgnoreStringInTitle.includes(lowercaseWord) && 
			word.length > 1 && 
			!/^\d+$/.test(word)) {  // 排除纯数字
			// 转换为首字母大写
			return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
		}
	}
	
	// 如果没有找到合适的单词,返回空字符串
	return '';
}

// Configuration
const CONFIG = {
	useJournal: GM_getValue('useJournal', true),  // Default use journal abbreviation
	debug: GM_getValue('debug', false)  // Debug mode
};

// Test data for debug mode
const DEFAULT_TEST_DATA = `@misc{chen2014l2modulispacessymplecticvortices,
      title={$L^2$-moduli spaces of symplectic vortices on Riemann surfaces with cylindrical ends}, 
      author={Bohui Chen and Bai-Ling Wang},
      year={2014},
      eprint={1405.6387},
      archivePrefix={arXiv},
      primaryClass={math.SG},
      url={https://arxiv.org/abs/1405.6387}, 
}`;

// Get stored test data or use default
let TEST_DATA = GM_getValue('testData', DEFAULT_TEST_DATA);

// Function to update test data
function updateTestData(newData) {
	TEST_DATA = newData;
	GM_setValue('testData', newData);
}

// 注册菜单命令
GM_registerMenuCommand('Toggle Journal/Title Mode', function() {
	const currentMode = GM_getValue('useJournal', true);
	const newMode = !currentMode;
	GM_setValue('useJournal', newMode);
	CONFIG.useJournal = newMode;
	
	// 显示当前模式
	const modeText = newMode ? 'Journal Mode' : 'Title Mode';
	alert('Switched to ' + modeText);
	
	// 刷新页面以应用新设置
	location.reload();
});

// Add status indicator
function addStatusIndicator() {
	// Remove existing indicator if any
	const existingIndicator = document.getElementById('mode-indicator');
	if (existingIndicator) {
		existingIndicator.remove();
	}

	const indicator = document.createElement('div');
	indicator.id = 'mode-indicator';
	indicator.style.cssText = `
		position: fixed;
		top: 10px;
		right: 10px;
		padding: 5px 10px;
		background: #f0f0f0;
		border: 1px solid #ccc;
		border-radius: 3px;
		font-size: 12px;
		z-index: 9999;
		cursor: pointer;
		user-select: none;
		margin-bottom: 5px;
	`;
	indicator.textContent = CONFIG.useJournal ? 'Mode: Journal' : 'Mode: Title';
	
	// Add click handler to toggle mode
	indicator.addEventListener('click', function() {
		const currentMode = GM_getValue('useJournal', true);
		const newMode = !currentMode;
		GM_setValue('useJournal', newMode);
		CONFIG.useJournal = newMode;
		
		// Update indicator text
		this.textContent = newMode ? 'Mode: Journal' : 'Mode: Title';
		
		// Update citation keys immediately
		updateBibTeXEntries();
	});
	
	document.body.appendChild(indicator);
}

// Function to update all BibTeX entries
function updateBibTeXEntries() {
	const els = document.getElementsByTagName('pre');
	for (let i = 0; i < els.length; i++) {
		try {
			const el = els[i];
			const bibdata = parseBibTex(el.innerHTML);
			if (!bibdata) continue;

			// Extract author
			const author = cleanAuthorName(bibdata.author);
			
			// Extract year
			let year = '';
			if (bibdata.year) {
				let yearMatch = bibdata.year.match(/\d{4}/);
				if (yearMatch) {
					year = yearMatch[0];
				}
			}
			
			// Get identifier based on current mode
			let identifier = '';
			if (CONFIG.useJournal && bibdata.journal) {
				identifier = getJournalAbbrev(bibdata.journal);
				if (!identifier) {
					identifier = cleanTitle(bibdata.title);
				}
			} else {
				identifier = cleanTitle(bibdata.title);
			}
			
			// Create new BibTeX key
			const bibkey = author + year + identifier;
			
			// Replace the citation key in the original text
			const originalText = el.innerHTML;
			const newText = originalText.replace(/@([^{]+){([^,\n]+)[,\n]/, `@$1{${bibkey},`);
			
			// Update the content
			el.innerHTML = newText;
			
			// Add click to copy functionality if not already present
			if (!el.hasAttribute('data-click-handler')) {
				el.setAttribute('data-click-handler', 'true');
				el.addEventListener('click', function() {
					try {
						var bibdata_lb = this.innerHTML
							.replace(/\r|\n/g, '\r\n')
							.replace(/^\r\n/g, '')
							.replace(/\s*$/g, '\r\n')
							.replace(/\r\n\r\n/g, '\r\n');
						GM_setClipboard(bibdata_lb);
					} catch (error) {
						console.error('Error copying to clipboard:', error);
					}
				});
			}
		} catch (error) {
			console.error('Error updating BibTeX entry:', error);
		}
	}
}

// 在页面加载完成后添加状态指示器
window.addEventListener('load', function() {
	addStandardizeButton();
	addStatusIndicator();
	addDebugToggle();
	addTestDataButton();
	// Update all BibTeX entries on page load
	updateBibTeXEntries();
});

// 添加新的期刊处理函数
function getJournalAbbrev(journal) {
	if (!journal) return '';
	
	// 移除LaTeX命令和特殊字符
	journal = journal.replace(/\\[a-zA-Z]+/g, '')     // 移除LaTeX命令
		   .replace(/[{}\\\'"`]/g, '')                // 移除特殊字符
		   .replace(/\([^)]*\)/g, '')                 // 移除括号内容
		   .replace(/\{[^}]*\}/g, '')                 // 移除花括号内容
		   .trim();
	
	// 分割成单词
	let words = journal.split(/[\s.]+/).filter(word => word.length > 0);
	
	if (words.length === 1) {
		// 如果只有一个单词,取前三个字母并转为大写
		return words[0].slice(0, 3).toUpperCase();
	} else {
		// 多个单词时提取大写字母
		let abbrev = journal.match(/[A-Z]/g);
		return abbrev ? abbrev.join('') : '';
	}
}

/**
 * auto set bibtex item checked for mrlookup site
 */
var url = location.pathname;
if (url.includes('mrlookup')) {
	document.getElementsByName('format')[1].checked = true;
}

// Add button to standardize BibTeX
function addStandardizeButton() {
	const button = document.createElement('button');
	button.textContent = 'Standardize BibTeX';
	button.style.cssText = `
		position: fixed;
		top: 45px;
		right: 10px;
		padding: 5px 10px;
		background: #4CAF50;
		color: white;
		border: none;
		border-radius: 3px;
		cursor: pointer;
		z-index: 9999;
		margin-bottom: 5px;
	`;
	button.addEventListener('click', standardizeBibTeX);
	document.body.appendChild(button);
}

// Add test data button when debug is on
function addTestDataButton() {
	// Remove existing test button if any
	const existingBtn = document.getElementById('test-data-btn');
	if (existingBtn) {
		existingBtn.remove();
	}
	
	// Only add button if debug mode is on
	if (!CONFIG.debug) return;
	
	const testBtn = document.createElement('button');
	testBtn.id = 'test-data-btn';
	testBtn.textContent = 'Test Data';
	testBtn.style.cssText = `
		position: fixed;
		top: 115px;
		right: 10px;
		padding: 5px 10px;
		background: #4CAF50;
		color: white;
		border: none;
		border-radius: 3px;
		cursor: pointer;
		z-index: 9999;
		margin-bottom: 5px;
	`;
	testBtn.addEventListener('click', () => {
		// First open the dialog
		standardizeBibTeX();
		// Then fill it with test data
		setTimeout(() => {
			const textarea = document.querySelector('textarea');
			if (textarea) {
				textarea.value = TEST_DATA;
			}
		}, 100); // Small delay to ensure dialog is created
	});
	document.body.appendChild(testBtn);
}

// Add debug mode toggle
function addDebugToggle() {
	const debugBtn = document.createElement('button');
	debugBtn.textContent = CONFIG.debug ? 'Debug: ON' : 'Debug: OFF';
	debugBtn.style.cssText = `
		position: fixed;
		top: 80px;
		right: 10px;
		padding: 5px 10px;
		background: ${CONFIG.debug ? '#ff4444' : '#f0f0f0'};
		color: ${CONFIG.debug ? 'white' : 'black'};
		border: 1px solid #ccc;
		border-radius: 3px;
		cursor: pointer;
		z-index: 9999;
		margin-bottom: 5px;
	`;
	debugBtn.addEventListener('click', function() {
		CONFIG.debug = !CONFIG.debug;
		GM_setValue('debug', CONFIG.debug);
		this.textContent = CONFIG.debug ? 'Debug: ON' : 'Debug: OFF';
		this.style.background = CONFIG.debug ? '#ff4444' : '#f0f0f0';
		this.style.color = CONFIG.debug ? 'white' : 'black';
		// Update test data button visibility
		addTestDataButton();
	});
	document.body.appendChild(debugBtn);
}

// Show debug result
function showDebugResult(result) {
	if (!CONFIG.debug) return;
	
	// Remove existing debug result if any
	const existingResult = document.getElementById('debug-result');
	if (existingResult) {
		existingResult.remove();
	}
	
	// Create overlay
	const overlay = document.createElement('div');
	overlay.style.cssText = `
		position: fixed;
		top: 0;
		left: 0;
		right: 0;
		bottom: 0;
		background: rgba(0,0,0,0.5);
		z-index: 10000;
	`;
	
	const resultDiv = document.createElement('div');
	resultDiv.id = 'debug-result';
	resultDiv.style.cssText = `
		position: fixed;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		background: white;
		padding: 20px;
		border-radius: 5px;
		box-shadow: 0 2px 10px rgba(0,0,0,0.2);
		z-index: 10001;
		width: 80%;
		max-width: 800px;
		max-height: 80vh;
		overflow: auto;
	`;
	
	const pre = document.createElement('pre');
	pre.style.cssText = `
		white-space: pre-wrap;
		word-wrap: break-word;
		font-family: monospace;
		font-size: 14px;
		margin: 0;
		padding: 10px;
		background: #f8f8f8;
		border-radius: 3px;
	`;
	pre.textContent = result;
	
	const closeBtn = document.createElement('button');
	closeBtn.textContent = 'Close';
	closeBtn.style.cssText = `
		position: absolute;
		top: 10px;
		right: 10px;
		padding: 5px 15px;
		background: #f0f0f0;
		border: 1px solid #ccc;
		border-radius: 3px;
		cursor: pointer;
	`;
	closeBtn.onclick = () => {
		document.body.removeChild(overlay);
	};
	
	// Close when clicking outside
	overlay.addEventListener('click', (event) => {
		if (event.target === overlay) {
			document.body.removeChild(overlay);
		}
	});
	
	resultDiv.appendChild(closeBtn);
	resultDiv.appendChild(pre);
	overlay.appendChild(resultDiv);
	document.body.appendChild(overlay);
}

// Modify standardizeBibTeX to handle debug mode
function standardizeBibTeX() {
	// Create dialog container
	const dialog = document.createElement('div');
	dialog.style.cssText = `		position: fixed;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		background: white;
		padding: 20px;
		border-radius: 5px;
		box-shadow: 0 2px 10px rgba(0,0,0,0.2);
		z-index: 10000;
		width: 80%;
		max-width: 800px;
	`;

	// Create textarea
	const textarea = document.createElement('textarea');
	textarea.style.cssText = `
		width: 100%;
		height: 200px;
		margin: 10px 0;
		padding: 10px;
		border: 1px solid #ccc;
		border-radius: 3px;
		font-family: monospace;
		font-size: 14px;
		resize: vertical;
	`;
	textarea.placeholder = 'Paste your BibTeX entry here...';

	// Create buttons container
	const buttons = document.createElement('div');
	buttons.style.cssText = `
		text-align: right;
		margin-top: 10px;
	`;

	// Create cancel button
	const cancelBtn = document.createElement('button');
	cancelBtn.textContent = 'Cancel';
	cancelBtn.style.cssText = `
		padding: 5px 15px;
		margin-right: 10px;
		background: #f0f0f0;
		border: 1px solid #ccc;
		border-radius: 3px;
		cursor: pointer;
	`;

	// Create submit button
	const submitBtn = document.createElement('button');
	submitBtn.textContent = 'Standardize';
	submitBtn.style.cssText = `
		padding: 5px 15px;
		background: #4CAF50;
		color: white;
		border: none;
		border-radius: 3px;
		cursor: pointer;
	`;

	// Create overlay
	const overlay = document.createElement('div');
	overlay.style.cssText = `
		position: fixed;
		top: 0;
		left: 0;
		right: 0;
		bottom: 0;
		background: rgba(0,0,0,0.5);
		z-index: 9999;
	`;

	// Add event listeners
	cancelBtn.onclick = () => {
		document.body.removeChild(overlay);
	};

	// Close dialog when clicking outside
	overlay.addEventListener('click', (event) => {
		// Only close if clicking directly on the overlay, not its children
		if (event.target === overlay) {
			document.body.removeChild(overlay);
		}
	});

	submitBtn.onclick = () => {
		const input = textarea.value.trim();
		if (!input) {
			alert('Please enter a BibTeX entry');
			return;
		}

		try {
			// Store the input as test data if in debug mode
			if (CONFIG.debug) {
				updateTestData(input);
			}

			// Parse the input BibTeX
			const bibdata = parseBibTex(input);
			if (!bibdata) {
				alert('Invalid BibTeX format');
				return;
			}

			// Clean up the data by removing extra braces and preserving math
			const cleanValue = (value) => {
				if (!value) return '';
				// Remove only the outermost braces if they exist
				// This preserves all LaTeX commands and math formulas
				return value.replace(/^{|}$/g, '');
			};

			// Extract author
			const author = cleanAuthorName(cleanValue(bibdata.author));
			
			// Extract year
			let year = '';
			if (bibdata.year) {
				let yearMatch = cleanValue(bibdata.year).match(/\d{4}/);
				if (yearMatch) {
					year = yearMatch[0];
				}
			}
			
			// Get identifier based on current mode
			let identifier = '';
			if (CONFIG.useJournal && bibdata.journal) {
				identifier = getJournalAbbrev(cleanValue(bibdata.journal));
				if (!identifier) {
					identifier = cleanTitle(cleanValue(bibdata.title));
				}
			} else {
				identifier = cleanTitle(cleanValue(bibdata.title));
			}
			
			// Create new BibTeX key
			const bibkey = author + year + identifier;

			// Get all field names from the input, preserving their original case
			const fieldNames = Object.keys(bibdata).filter(key => 
				key !== 'typeName' && key !== 'citationKey'
			).map(key => key.toUpperCase());

			// Find the longest field name for alignment
			const maxLength = Math.max(...fieldNames.map(name => name.length));

			// Function to format a field with proper alignment
			const formatField = (name, value) => {
				const padding = ' '.repeat(maxLength - name.length);
				// Clean the value and ensure it's properly wrapped in braces
				const cleanedValue = cleanValue(value);
				return `    ${name}${padding} = {${cleanedValue}},\n`;
			};

			// Standardize the format
			let standardized = `@${bibdata.typeName} {${bibkey},\n`;
			
			// Add all fields from the input
			for (const field of fieldNames) {
				const value = bibdata[field.toLowerCase()];
				if (value) {
					standardized += formatField(field, value);
				}
			}

			// Remove trailing comma and add closing brace
			standardized = standardized.replace(/,\n$/, '\n}');

			if (CONFIG.debug) {
				// Show debug result
				showDebugResult(standardized);
			} else {
				// Copy to clipboard
				GM_setClipboard(standardized);
				alert('Standardized BibTeX has been copied to clipboard!');
			}
			document.body.removeChild(overlay);
		} catch (error) {
			console.error('Error standardizing BibTeX:', error);
			alert('Error standardizing BibTeX. Please check the console for details.');
		}
	};

	// Assemble dialog
	buttons.appendChild(cancelBtn);
	buttons.appendChild(submitBtn);
	dialog.appendChild(textarea);
	dialog.appendChild(buttons);
	overlay.appendChild(dialog);
	document.body.appendChild(overlay);

	// Focus textarea
	textarea.focus();
}