// ==UserScript== // @name 选中并分享(带锚点自动跳转) // @namespace http://tampermonkey.net/ // @version 0.1 // @description 选中文字后弹出工具条,一键复制 HTML 富文本链接(OneNote)或 Markdown 链接(Typora) // @author You // @match *://*/* // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; GM_addStyle(` .smart-copy-toolbar { position: fixed; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(12px); border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 8px; display: flex; gap: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; border: 1px solid rgba(0,0,0,0.06); animation: popIn 0.2s ease; user-select: none; } @keyframes popIn { from { opacity: 0; transform: translate(-50%, 8px) scale(0.96); } to { opacity: 1; transform: translate(-50%, 0) scale(1); } } .sc-btn { border: none; background: #f5f5f7; padding: 10px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500; color: #1d1d1f; transition: all 0.2s; display: flex; align-items: center; gap: 6px; } .sc-btn:hover { background: #e8e8ed; transform: translateY(-1px); } .sc-btn.primary { background: #0071e3; color: white; } .sc-btn.primary:hover { background: #0077ed; } .sc-toast { position: fixed; bottom: 20%; left: 50%; transform: translateX(-50%) translateY(10px); background: rgba(0,0,0,0.8); color: white; padding: 12px 20px; border-radius: 24px; font-size: 14px; z-index: 2147483647; opacity: 0; transition: all 0.3s; pointer-events: none; backdrop-filter: blur(8px); } .sc-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } `); let toolbar = null, toastTimer = null; function convertHtmlChar(str) { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function encodeUrlSpecialChar(url) { return url.replace(/ /g, '%20').replace(/"/g, '%22'); } function hasTextFragment(url) { return /#.*:~:text=/.test(url); } function createTextFragment(text) { let clean = text.replace(/\s+/g, ' ').trim(); const chars = Array.from(clean); if (chars.length > 60) clean = chars.slice(0, 60).join(''); return clean; } function buildUrlWithFragment(baseUrl, selectedText) { if (!selectedText || selectedText.length < 2) return baseUrl; const fragment = createTextFragment(selectedText); if (!fragment) return baseUrl; try { const url = new URL(baseUrl); url.hash = `:~:text=${encodeURIComponent(fragment)}`; return url.toString(); } catch (e) { const base = baseUrl.split('#')[0]; return `${base}#:~:text=${encodeURIComponent(fragment)}`; } } function createHtmlLink(content, url) { const hasFragment = hasTextFragment(url); const safeContent = convertHtmlChar(content); const safeUrl = convertHtmlChar(encodeUrlSpecialChar(url)); let attr = 'target="_blank"'; if (hasFragment) attr += ' rel="noopener"'; return `${safeContent}`; } async function writeToClipboard(plainText, htmlText) { if (navigator.clipboard && window.ClipboardItem) { try { await navigator.clipboard.write([ new ClipboardItem({ 'text/plain': new Blob([plainText], { type: 'text/plain' }), 'text/html': new Blob([htmlText], { type: 'text/html' }) }) ]); return true; } catch (err) { console.error('Clipboard API failed:', err); } } try { await navigator.clipboard.writeText(plainText); return true; } catch (err) { return false; } } function showToast(msg) { document.querySelector('.sc-toast')?.remove(); const t = document.createElement('div'); t.className = 'sc-toast'; t.textContent = msg; document.body.appendChild(t); requestAnimationFrame(() => t.classList.add('show')); clearTimeout(toastTimer); toastTimer = setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 300); }, 2500); } function hideToolbar() { if (!toolbar) return; toolbar.style.opacity = '0'; setTimeout(() => { toolbar?.remove(); toolbar = null; }, 150); } function getSelectionRect() { const sel = window.getSelection(); if (!sel.rangeCount) return null; const rect = sel.getRangeAt(0).getBoundingClientRect(); return (rect.width || rect.height) ? rect : null; } function showToolbar(rect) { hideToolbar(); const selected = window.getSelection().toString().trim(); if (!selected) return; const baseUrl = location.href.split('#')[0]; const url = buildUrlWithFragment(baseUrl, selected); toolbar = document.createElement('div'); toolbar.className = 'smart-copy-toolbar'; let top = rect.top - 55; if (top < 10) top = rect.bottom + 15; toolbar.style.cssText = `left:${rect.left + rect.width/2}px;top:${top}px;transform:translateX(-50%)`; // 按钮1:HTML(富文本,用于 OneNote) const btnHtml = document.createElement('button'); btnHtml.className = 'sc-btn primary'; btnHtml.innerHTML = '📋 HTML'; btnHtml.onclick = async (e) => { e.stopPropagation(); // HTML 富文本:链接文本 = 选中的内容 const htmlContent = createHtmlLink(selected, url); // Plain fallback 也是 Markdown 格式,方便万一 const plainFallback = `[${selected}](${url})`; const ok = await writeToClipboard(plainFallback, htmlContent); showToast(ok ? '已复制 HTML 链接(适用于 OneNote)' : '复制失败'); hideToolbar(); }; // 按钮2:Markdown(用于 Typora) const btnMd = document.createElement('button'); btnMd.className = 'sc-btn'; btnMd.innerHTML = '📝 Markdown'; btnMd.onclick = async (e) => { e.stopPropagation(); // 纯 Markdown 链接,无引用块 const markdown = `[${selected.replace(/([[\]])/g, '\\$1')}](${url})`; try { await navigator.clipboard.writeText(markdown); showToast('已复制 Markdown 链接(适用于 Typora)'); } catch (err) { showToast('复制失败'); } hideToolbar(); }; toolbar.append(btnHtml, btnMd); document.body.appendChild(toolbar); } document.addEventListener('mouseup', (e) => { if (toolbar?.contains(e.target)) return; setTimeout(() => { const sel = window.getSelection().toString().trim(); sel ? showToolbar(getSelectionRect()) : hideToolbar(); }, 10); }); document.addEventListener('mousedown', (e) => { if (!toolbar?.contains(e.target)) hideToolbar(); }); window.addEventListener('scroll', hideToolbar, { passive: true }); document.addEventListener('keydown', (e) => e.key === 'Escape' && hideToolbar()); })();