// ==UserScript== // @name 标题批量导出DBLP的BibTeX // @namespace http://tampermonkey.net/ // @version 1.5 // @description 在网页左下角生成一个按钮,从dblp中获取选定文本的BibTeX并复制到剪贴板。支持批量获取,支持从剪贴板读取,支持随时下载,支持导出URL和CSV。 // @author shandianchengzi // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @license GPL-3.0 // @downloadURL https://update.greasyfork.icu/scripts/522825/%E6%A0%87%E9%A2%98%E6%89%B9%E9%87%8F%E5%AF%BC%E5%87%BADBLP%E7%9A%84BibTeX.user.js // @updateURL https://update.greasyfork.icu/scripts/522825/%E6%A0%87%E9%A2%98%E6%89%B9%E9%87%8F%E5%AF%BC%E5%87%BADBLP%E7%9A%84BibTeX.meta.js // ==/UserScript== // Inject Custom CSS const css = ` #dblp-batch-overlay { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.85); color: white; padding: 25px; border-radius: 10px; z-index: 100000; text-align: center; min-width: 400px; max-width: 90%; backdrop-filter: blur(5px); box-shadow: 0 4px 15px rgba(0,0,0,0.3); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; display: none; } #dblp-batch-title { font-size: 18px; font-weight: bold; margin-bottom: 15px; color: #fff; } #dblp-batch-current { font-size: 14px; margin-bottom: 20px; color: #ccc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 450px; margin-left: auto; margin-right: auto; min-height: 20px; } .dblp-btn { border: none; border-radius: 6px; padding: 8px 15px; cursor: pointer; font-weight: bold; transition: all 0.2s; margin: 5px; outline: none; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; } #dblp-btn-download { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } #dblp-btn-download:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(118, 75, 162, 0.4); } #dblp-btn-copy-urls { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; } #dblp-btn-copy-urls:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(56, 239, 125, 0.4); } #dblp-btn-csv { background: linear-gradient(135deg, #ff9966 0%, #ff5e62 100%); color: white; } #dblp-btn-csv:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(255, 94, 98, 0.4); } #dblp-btn-close { background: rgba(255, 255, 255, 0.15); color: #ddd; border: 1px solid rgba(255,255,255,0.2); } #dblp-btn-close:hover { background: rgba(255, 255, 255, 0.25); color: white; } /* Confirm Modal */ #dblp-confirm-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; color: #333; padding: 20px; border-radius: 8px; z-index: 100001; box-shadow: 0 10px 25px rgba(0,0,0,0.5); text-align: center; max-width: 400px; display: none; } #dblp-confirm-text { background: #f5f5f5; padding: 10px; margin: 10px 0; border-radius: 4px; font-family: monospace; text-align: left; max-height: 100px; overflow-y: auto; font-size: 12px; } `; if (typeof GM_addStyle !== 'undefined') { GM_addStyle(css); } else { const styleNode = document.createElement('style'); styleNode.innerHTML = css; document.head.appendChild(styleNode); } // Toast function function Toast(msg, duration) { duration = isNaN(duration) ? 3000 : duration; var m = document.createElement('div'); m.innerHTML = msg; m.style.cssText = "font-family: 'siyuan'; max-width: 60%; min-width: 150px; padding: 10px 14px; height: auto; color: rgb(255, 255, 255); line-height: 1.5; text-align: center; border-radius: 4px; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 999999; background: rgba(0, 0, 0, 0.7); font-size: 16px;"; document.body.appendChild(m); setTimeout(function() { m.style.transition = 'opacity 0.5s ease-in'; m.style.opacity = '0'; setTimeout(function() { if(m.parentNode) document.body.removeChild(m); }, 500); }, duration); } var headers = { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36", 'Referer': 'https://dblp.org/' }; (function() { 'use strict'; var lang=navigator.appName=="Netscape"?navigator.language:navigator.userLanguage; var lang_hint = { error_no_text: "没有选中文本且剪贴板为空!", clipboard_confirm: "未选中文本。是否使用剪贴板内容?", clipboard_read_err: "无法读取剪贴板,请手动选择文本。", fetching_one: "正在获取...", done_copy: "已完成并复制!", batch_title: (cur, total) => `批量提取中: ${cur} / ${total}`, batch_done_title: "批量提取完成", download_btn: "下载 BibTeX (.bib)", copy_urls_btn: "仅复制 URL", csv_btn: "下载表格 (.csv)", close_btn: "关闭面板", current_prefix: "正在搜索: ", default_btn: "Get BibTeX", urls_copied: "URLs 已复制到剪贴板!" }; if (!lang.startsWith('zh')) { lang_hint = { error_no_text: "No text selected and clipboard is empty!", clipboard_confirm: "No text selected. Use clipboard content?", clipboard_read_err: "Cannot read clipboard.", fetching_one: "Fetching...", done_copy: "Done & Copied!", batch_title: (cur, total) => `Processing: ${cur} / ${total}`, batch_done_title: "Batch Complete", download_btn: "Download BibTeX", copy_urls_btn: "Copy URLs", csv_btn: "Download CSV", close_btn: "Close", current_prefix: "Searching: ", default_btn: "Get BibTeX", urls_copied: "URLs copied to clipboard!" }; } // --- UI Elements Creation --- // 1. Trigger Button const button = document.createElement('button'); button.innerText = lang_hint.default_btn; button.style.cssText = "position: fixed; bottom: 10px; left: 10px; z-index: 9999; padding: 10px; background-color: #007BFF; color: white; border: none; border-radius: 5px; cursor: pointer; white-space: pre; text-align: center;"; document.body.appendChild(button); // 2. Batch Overlay const overlay = document.createElement('div'); overlay.id = 'dblp-batch-overlay'; overlay.innerHTML = `
`; document.body.appendChild(overlay); // 3. Confirm Modal const confirmModal = document.createElement('div'); confirmModal.id = 'dblp-confirm-modal'; confirmModal.innerHTML = `
${lang_hint.clipboard_confirm}
`; document.body.appendChild(confirmModal); // --- Logic Variables --- let batchResults = []; // Stores BibTeX strings let batchLines = []; // Stores original queries let isBatchProcessing = false; // --- Helper Functions --- // Robust function to extract fields from BibTeX (handles nested braces and multi-lines) function extractBibField(bibtex, fieldName) { if (!bibtex || bibtex === "None") return "None"; // 1. Locate "fieldName =" or "fieldName =" ignoring case const regex = new RegExp(`${fieldName}\\s*=\\s*\\{`, "i"); const match = bibtex.match(regex); if (!match) return "None"; // 2. Iterate characters to find the matching closing brace let openCount = 1; let content = ""; // Start after the opening '{' let startIndex = match.index + match[0].length; for (let i = startIndex; i < bibtex.length; i++) { const char = bibtex[i]; if (char === '{') { openCount++; } else if (char === '}') { openCount--; } if (openCount === 0) { break; } content += char; } // 3. Clean up the extracted content // Replace newlines and multiple spaces with a single space // Also remove any surrounding braces if they were part of formatting (e.g. {{Title}}) -> {Title} // But usually we just want the raw content. The loop extracts everything INSIDE the outer field braces. return content.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim(); } function downloadContent(content, filename) { const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename || 'download.txt'; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function fetchBibTeX(query, silent, callback) { const searchUrl = `https://dblp.org/search?q=${encodeURIComponent(query)}`; GM_xmlhttpRequest({ method: 'GET', url: searchUrl, headers: headers, onload: function(response) { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, 'text/html'); const bibLink = doc.querySelector('a[href*="?view=bibtex"]'); if (!bibLink) { if(!silent) Toast("BibTeX Not Found"); callback("None"); return; } const bibUrl = bibLink.href.replace('.html?view=bibtex', '.bib'); GM_xmlhttpRequest({ method: 'GET', url: bibUrl, headers: headers, onload: function(bibResponse) { callback(bibResponse.responseText); }, onerror: function() { if(!silent) Toast("Error fetching bib file"); callback("None"); } }); }, onerror: function() { if(!silent) Toast("Error searching DBLP"); callback("None"); } }); } // --- Event Handlers --- const titleEl = document.getElementById('dblp-batch-title'); const currentEl = document.getElementById('dblp-batch-current'); const downloadBtn = document.getElementById('dblp-btn-download'); const csvBtn = document.getElementById('dblp-btn-csv'); const copyUrlsBtn = document.getElementById('dblp-btn-copy-urls'); const closeBtn = document.getElementById('dblp-btn-close'); // Helper to get valid results so far function getResultsSoFar() { // Return objects { line, bib } only for processed items return batchLines.map((line, idx) => ({ line: line, bib: batchResults[idx] })).filter(item => item.bib !== null && item.bib !== undefined); } downloadBtn.onclick = () => { const results = getResultsSoFar(); if(results.length === 0) { Toast("Nothing fetched yet."); return; } const content = results.map(r => r.bib === "None" ? `% Failed to fetch: ${r.line}` : r.bib).join('\n\n'); downloadContent(content, 'dblp_bibtex.bib'); }; copyUrlsBtn.onclick = () => { const results = getResultsSoFar(); if(results.length === 0) { Toast("Nothing fetched yet."); return; } const urlList = results.map(r => { if (r.bib === "None") return "None"; return extractBibField(r.bib, "url"); }).join('\n'); GM_setClipboard(urlList); Toast(lang_hint.urls_copied); }; csvBtn.onclick = () => { const results = getResultsSoFar(); if(results.length === 0) { Toast("Nothing fetched yet."); return; } // CSV Header // BOM (\uFEFF) is added so Excel opens it in UTF-8 correctly let csvContent = "\uFEFF原始搜索词,提取标题,URL,BibTeX\n"; // Escape CSV value: wrap in quotes, escape double quotes as "" const esc = (val) => { if (val === null || val === undefined) return ""; val = String(val); if (val.search(/("|,|\n|\r)/g) >= 0) { return `"${val.replace(/"/g, '""')}"`; } return val; }; csvContent += results.map(r => { if (r.bib === "None") { return `${esc(r.line)},None,None,None`; } const title = extractBibField(r.bib, "title"); const url = extractBibField(r.bib, "url"); return `${esc(r.line)},${esc(title)},${esc(url)},${esc(r.bib)}`; }).join('\n'); downloadContent(csvContent, 'dblp_results.csv'); }; closeBtn.onclick = () => { overlay.style.display = 'none'; isBatchProcessing = false; // Stop if closed early? Or just hide. }; // Clipboard Confirm Logic function askClipboard(text) { return new Promise((resolve) => { document.getElementById('dblp-confirm-text').innerText = text.length > 200 ? text.substring(0, 200) + '...' : text; confirmModal.style.display = 'block'; document.getElementById('dblp-confirm-yes').onclick = () => { confirmModal.style.display = 'none'; resolve(true); }; document.getElementById('dblp-confirm-no').onclick = () => { confirmModal.style.display = 'none'; resolve(false); }; }); } button.addEventListener('click', async () => { if (isBatchProcessing) { Toast("正在批量处理中,请使用中间面板控制"); return; } let selection = window.getSelection().toString().trim(); // Fallback to clipboard if (!selection) { try { const clipText = await navigator.clipboard.readText(); if (clipText && clipText.trim()) { const useClip = await askClipboard(clipText.trim()); if (useClip) { selection = clipText.trim(); } else { return; } } else { Toast(lang_hint.error_no_text); return; } } catch (e) { Toast(lang_hint.clipboard_read_err); return; } } if (!selection) return; const lines = selection.split(/[\r\n]+/).map(s => s.trim()).filter(s => s); if (lines.length === 0) return; if (lines.length === 1) { // Single Mode Toast(lang_hint.fetching_one, 1000); fetchBibTeX(lines[0], false, (res) => { if (res && res !== "None") { GM_setClipboard(res); Toast(lang_hint.done_copy); } else { Toast("Failed: " + lines[0]); } }); } else { // Batch Mode isBatchProcessing = true; batchLines = lines; batchResults = new Array(lines.length).fill(null); let completedCount = 0; // Init UI overlay.style.display = 'block'; closeBtn.style.display = 'none'; // All buttons visible now to support "Download while fetching" // We assume user knows that incomplete items won't be in the download titleEl.innerText = lang_hint.batch_title(0, lines.length); currentEl.innerText = "Initializing..."; lines.forEach((line, index) => { setTimeout(() => { if (!isBatchProcessing) return; // Stop if canceled/closed logic added later currentEl.innerText = lang_hint.current_prefix + line; fetchBibTeX(line, true, (result) => { batchResults[index] = result === "None" ? "None" : result; completedCount++; if (isBatchProcessing) { titleEl.innerText = lang_hint.batch_title(completedCount, lines.length); } if (completedCount === lines.length) { isBatchProcessing = false; titleEl.innerText = lang_hint.batch_done_title; currentEl.innerText = ""; closeBtn.style.display = 'inline-block'; // Auto Copy BibTeX (optional, maybe annoying for huge lists, but requested in v1) const finalContent = batchResults.map(r => r === "None" ? "% Failed" : r).join('\n\n'); GM_setClipboard(finalContent); Toast(lang_hint.done_copy); } }); }, index * 800); }); } }); // Toggle button visibility menu GM_registerMenuCommand(lang === "zh-CN" ? "显示/隐藏按钮" : "Show/Hide Button", function() { button.style.display = button.style.display === 'none' ? 'block' : 'none'; GM_setValue('showButton', button.style.display === 'block'); }); if (!GM_getValue('showButton', true)) { button.style.display = 'none'; } })();