xxhk.org

学习骇客

油猴脚本:Highlight to Anki 免费版 Readwise

2024 / 9 / 19

前提知识:油猴脚本的用法

功能

在浏览器右边增加一个侧边栏,默认为隐藏状态,可以切换为显示或隐藏状态。显示按钮可以被上下拖动,防止挡住内容。

按住 Alt 键的同时,用鼠标选择内容,被选中内容会被摘录到侧边栏,原来的被选中内容也会被标记为高亮状态。

在侧边栏里,按住 Alt 键的同时单击单个笔记,可以将其单个移除,点击右上角的清空按钮可以侧边栏里的内容全部清空。总结用法就是 Alt 键负责原内容。

按住 Ctrl+Alt 键的同时,用鼠标选择内容,会弹出输入框,用于添加批注,输入完后按下回车键即可将批注和被选中的内容保存到侧边栏,被选中的内容同样会被标记为高亮。

在侧边栏里,按住 Ctrl 键的同时单击单个笔记,可以进入编辑状态。总结用法就是 Ctrl 键负责自定义内容。

侧边栏的编辑支持通过快捷键 Ctrl+B、Ctrl+I、Ctrl+U 设置加粗、斜体和下划线样式,配合 Anki 模板可以实现笔记里的高亮挖空效果。

支持选中文字和图片。如果保存的是动图,在 Anki 也会显示为动图。

支持跨多个页面的连续保存,不受浏览器关闭和电脑关机的影响。

摘录完毕后下载 .txt 文件,配合浏览划线的模板 ,即可在 Anki 学习了。

支持在 Anki 里跳转回到原文

局限

在禁止复制的页面无效,例如微信读书网页版等。

部分网页的源码里故意添加了断行等扰乱效果,暂时无法完美恢复。

连续选中多个段落时,高亮效果不稳定,但不影响保存。

特殊样式暂未保留,仅支持保存图文内容本身。

未在更多场景下测试,请大家暂时尽量在适合的场景下使用。

场景

浏览网页时保存有用信息。

读技术文档时,跨多个页面选择性摘录内容。

刷推特,频繁进入多个帖子的主页时,也可以连续摘录并集中保存。

学英语,阅读原文或使用“沉浸式翻译”插件进行双语阅读时,摘录句子并对生词进行挖空学习(查询后可不必再添加译文)。

等等……

进阶

主题学习:由于在摘录笔记时保留了原始链接,也就是笔记来源的网页,那么可以进入编辑状态,到 html 模式下复制这个域名的前面主要部分。然后粘贴到浏览窗口里搜索,为所有来自该网站的笔记创建筛选牌组,进行主题学习。这样甚至省去了手动添加主题标签的麻烦。

脚本

如有修复或更新,请移步至原文 获取更新后的脚本。

如有转载或改写,请注明来源。

// ==UserScript== // @name Highlight to Anki // @namespace https://xxhk.org/highlights-to-anki // @version 1.1 // @description 学习骇客原创,转载或改写请注明来源,详情与更新见上方第二行地址 // @author 学习骇客(xxhk.org) // @match *://*/* // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; const STORAGE_KEY = 'centralizedHighlightedText'; let highlightTimeout = null; let isHighlightEnabled = true; const currentHighlights = new Set(); let isSidebarVisible = false; // 移动到全局作用域 // 提取并格式化标题 function extractTitle() { let title = document.title.replace(/[\\/:*?"<>|]+/g, '-').replace(/\s+/g, '-').replace(/-+/g, '-'); return title; } const Htitle = extractTitle(); // 检查元素是否有背景图片 function hasBackgroundImage(element) { const bgImage = window.getComputedStyle(element).backgroundImage; return bgImage && bgImage !== 'none'; } // 生成唯一的高亮 ID function generateHighlightId() { return 'highlight-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); } // 在光标位置显示自定义输入框 function showCustomInputBox(position, element) { const rect = position; // 获取位置 const inputBox = document.createElement('input'); inputBox.type = 'text'; inputBox.placeholder = '请输入附加内容'; inputBox.style.position = 'absolute'; // 确保输入框在视口内 let top = rect.bottom + window.scrollY; let left = rect.left + window.scrollX; // 如果输入框超出视口底部,将其移动到上方 if (top + 30 > window.innerHeight + window.scrollY) { top = rect.top + window.scrollY - 40; // 40 是输入框的高度和偏移量 } // 如果输入框超出视口右侧,将其向左移动 if (left + 200 > window.innerWidth + window.scrollX) { left = window.innerWidth + window.scrollX - 210; // 210 是输入框的宽度和偏移量 } inputBox.style.top = `${top}px`; inputBox.style.left = `${left}px`; inputBox.style.zIndex = '10000'; // 确保输入框在最上层 inputBox.style.padding = '5px'; inputBox.style.border = '1px solid #ccc'; inputBox.style.borderRadius = '4px'; inputBox.style.backgroundColor = '#fff'; inputBox.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)'; inputBox.style.color = '#000'; // 确保文字颜色为黑色 // 设置输入框的宽度 inputBox.style.width = '400px'; // 将输入框添加到文档中 document.body.appendChild(inputBox); // 自动聚焦输入框 setTimeout(() => { inputBox.focus(); }, 0); // 处理回车键事件 inputBox.addEventListener('keydown', function(event) { if (event.key === 'Enter') { const userNote = inputBox.value; if (inputBox.parentNode) { document.body.removeChild(inputBox); // 移除输入框 } saveHighlights(true, userNote, element); } }); // 如果用户点击输入框外部,移除输入框 document.addEventListener('click', function(event) { if (event.target !== inputBox) { if (inputBox.parentNode) { document.body.removeChild(inputBox); } } }, { once: true }); } // 新增的函数:处理文本中的换行和多空格 function processSelectedText(text) { // 使用正则匹配换行前无标点且下一行以小写字母开头的情况 // 替换换行为单个空格 // 然后将多个空格合并为一个 return text.replace(/([^\.\!\?。!?])\n([a-z])/g, '$1 $2').replace(/\s+/g, ' '); } // 保存高亮的文本或图片到集中存储 function saveHighlights(saveHistory = false, userNote = '', element = null) { const highlights = []; const elementsToHighlight = element ? [element] : []; elementsToHighlight.forEach(elem => { const pageURL = window.location.href; let textContent = ''; let images = []; // 提取文本内容和图片 function extractContent(node) { node.childNodes.forEach(child => { if (child.nodeType === Node.TEXT_NODE) { textContent += child.textContent; } else if (child.nodeType === Node.ELEMENT_NODE) { if (child.tagName === 'IMG') { images.push(`<img src="${child.src}">`); } else { extractContent(child); } } }); } if (elem.tagName === 'IMG') { images.push(`<img src="${elem.src}">`); } else { extractContent(elem); } // 处理文本内容:替换换行和多空格 textContent = processSelectedText(textContent); // 移除重复的图片 images = [...new Set(images)]; const { text: formattedNote, images: formattedImages } = formatUserNote(userNote); let finalContent = ''; if (formattedNote) { finalContent += formattedNote; // 在内容前添加批注 } if (textContent.trim()) { if (finalContent) finalContent += '<br><br>'; // 添加段落分隔 // 将多个段落用 '<br><br>' 拼接 const paragraphs = textContent.trim().split(/\n+/).map(p => p.trim()).filter(p => p); finalContent += paragraphs.join('<br><br>'); } if (images.length > 0) { if (finalContent) finalContent += '<br><br>'; // 添加段落分隔 finalContent += images.join('<br><br>'); } if (formattedImages.length > 0) { if (finalContent) finalContent += '<br><br>'; // 添加段落分隔 finalContent += formattedImages.map(url => `<img src="${url}">`).join('<br><br>'); } finalContent += ` <a href="${pageURL}" target="_blank">*</a>`; // 确保唯一性 const highlightId = elem.getAttribute('data-highlight-id'); if (!currentHighlights.has(highlightId)) { currentHighlights.add(highlightId); let centralizedHighlights = JSON.parse(GM_getValue(STORAGE_KEY, '[]')); const isDuplicate = centralizedHighlights.some(item => item.id === highlightId); if (!isDuplicate) { highlights.push({ id: highlightId, html: finalContent, pageTitle: Htitle, pageURL: pageURL, note: userNote || '', }); // 将新高亮添加到存储中 centralizedHighlights.push(...highlights); GM_setValue(STORAGE_KEY, JSON.stringify(centralizedHighlights)); } } }); updateSidebar(JSON.parse(GM_getValue(STORAGE_KEY, '[]'))); // 更新侧边栏 } // 格式化用户批注(处理图片链接) function formatUserNote(userNote) { if (!userNote) return { text: '', images: [] }; const imageExtensions = ['jpg', 'jpeg', 'png', 'gif']; let images = []; let text = userNote; imageExtensions.forEach(ext => { const regex = new RegExp(`(https?://[^\\s]+\\.${ext})`, 'g'); const matches = userNote.match(regex); if (matches) { images = images.concat(matches); text = text.replace(regex, '').trim(); } }); return { text: text, images: images }; } // 更新侧边栏中的高亮内容 function updateSidebar(highlights) { const sidebarContent = document.getElementById('highlight-sidebar-content'); if (!sidebarContent) return; sidebarContent.innerHTML = ''; highlights.forEach((item, index) => { const highlightDiv = document.createElement('div'); highlightDiv.innerHTML = `${index + 1}. ${item.html}`; highlightDiv.style.borderBottom = '1px solid #ddd'; highlightDiv.style.fontSize = '75%'; // 调整字体大小为 75% highlightDiv.style.marginBottom = '5px'; highlightDiv.style.paddingRight = '10px'; highlightDiv.style.cursor = 'pointer'; highlightDiv.style.marginTop = '10px'; // 添加顶部边距作为分隔 // 限制图片高度 const imgs = highlightDiv.querySelectorAll('img'); imgs.forEach(img => { img.style.maxHeight = '2em'; // 将高度限制为两行文字 img.style.objectFit = 'contain'; }); highlightDiv.addEventListener('click', (event) => { if (event.altKey) { removeHighlightItem(index, item.id); } else if (event.ctrlKey) { editHighlightItem(index, item.id, highlightDiv); } }); sidebarContent.appendChild(highlightDiv); }); } // 移除指定的高亮项 function removeHighlightItem(index, highlightId) { let centralizedHighlights = JSON.parse(GM_getValue(STORAGE_KEY, '[]')); if (centralizedHighlights.length > index) { centralizedHighlights.splice(index, 1); GM_setValue(STORAGE_KEY, JSON.stringify(centralizedHighlights)); updateSidebar(centralizedHighlights); // 从页面中移除高亮 // 移除 hlak-sentence 标签 const hlakSentenceElements = document.querySelectorAll(`hlak-sentence[data-highlight-id='${highlightId}']`); hlakSentenceElements.forEach(elem => { const parent = elem.parentNode; while (elem.firstChild) { parent.insertBefore(elem.firstChild, elem); } parent.removeChild(elem); parent.normalize(); // 合并相邻的文本节点 }); // 移除 hlak-phrase 标签 const hlakPhraseElements = document.querySelectorAll(`hlak-phrase[data-highlight-id='${highlightId}']`); hlakPhraseElements.forEach(elem => { const parent = elem.parentNode; while (elem.firstChild) { parent.insertBefore(elem.firstChild, elem); } parent.removeChild(elem); parent.normalize(); // 合并相邻的文本节点 }); // 从 currentHighlights 中移除 currentHighlights.delete(highlightId); } } // 编辑指定的高亮项(包括批注和摘录内容),在原位置编辑并自动保存 function editHighlightItem(index, highlightId, highlightDiv) { let centralizedHighlights = JSON.parse(GM_getValue(STORAGE_KEY, '[]')); const itemIndex = centralizedHighlights.findIndex(item => item.id === highlightId); if (itemIndex >= 0) { const item = centralizedHighlights[itemIndex]; // 创建一个 contentEditable 的 div 来编辑内容 const editableDiv = document.createElement('div'); editableDiv.contentEditable = true; editableDiv.style.width = '100%'; editableDiv.style.minHeight = '100px'; editableDiv.style.border = '1px solid #ccc'; editableDiv.style.padding = '5px'; editableDiv.style.outline = 'none'; editableDiv.innerHTML = item.html; // 替换 highlightDiv 的内容为 editableDiv highlightDiv.innerHTML = ''; highlightDiv.appendChild(editableDiv); // 当 editableDiv 失去焦点时,自动保存 editableDiv.addEventListener('blur', () => { let newContent = editableDiv.innerHTML; // 处理连续的 <br> 标签 newContent = newContent.replace(/(\s*<br\s*\/?>\s*){3,}/gi, '<br><br>'); // 更新 item.html item.html = newContent; item.note = ''; // 清空批注 // 保存到存储 centralizedHighlights[itemIndex] = item; GM_setValue(STORAGE_KEY, JSON.stringify(centralizedHighlights)); // 更新侧边栏 updateSidebar(centralizedHighlights); }); // 聚焦到 editableDiv editableDiv.focus(); } } // 创建并添加控制按钮和侧边栏 function createSidebar() { const sidebar = document.createElement('div'); sidebar.id = 'highlight-sidebar'; sidebar.style.position = 'fixed'; sidebar.style.top = '0'; sidebar.style.right = '0'; sidebar.style.width = '300px'; sidebar.style.height = '100%'; sidebar.style.backgroundColor = '#f0f4f8'; sidebar.style.borderLeft = '1px solid #ddd'; sidebar.style.zIndex = '9999'; // 确保侧边栏在最上层 sidebar.style.overflowY = 'auto'; sidebar.style.padding = '0'; // 移除内边距 sidebar.style.lineHeight = '1.5'; sidebar.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)'; sidebar.style.color = '#34495e'; sidebar.style.resize = 'horizontal'; // 允许水平调整大小 sidebar.style.minWidth = '200px'; sidebar.style.maxWidth = '50%'; sidebar.style.boxSizing = 'border-box'; // 确保宽度包含内边距 sidebar.style.display = 'none'; // 默认隐藏 // 禁用侧边栏内容的翻译 sidebar.setAttribute('translate', 'no'); sidebar.classList.add('notranslate'); const sidebarHeader = document.createElement('div'); sidebarHeader.style.display = 'flex'; sidebarHeader.style.flexWrap = 'wrap'; sidebarHeader.style.justifyContent = 'space-between'; sidebarHeader.style.alignItems = 'center'; sidebarHeader.style.padding = '10px'; sidebarHeader.style.borderBottom = '1px solid #34495e'; sidebarHeader.style.backgroundColor = '#1abc9c'; sidebarHeader.style.color = '#fff'; sidebarHeader.style.border = 'none'; sidebarHeader.style.borderRadius = '0'; sidebarHeader.style.position = 'relative'; sidebarHeader.style.zIndex = '10001'; // 确保标题在最上层 const headerTitle = document.createElement('h3'); headerTitle.innerText = '✨ Highlight to Anki丨Help ✨'; headerTitle.style.margin = '0'; headerTitle.style.fontSize = '16px'; headerTitle.style.flex = '1 0 100%'; headerTitle.style.textAlign = 'center'; const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.flexWrap = 'wrap'; buttonContainer.style.marginTop = '5px'; buttonContainer.style.width = '100%'; const createButton = (text, bgColor, onClick) => { const btn = document.createElement('button'); btn.textContent = text; btn.style.margin = '2px'; btn.style.padding = '5px'; btn.style.backgroundColor = bgColor; btn.style.color = '#fff'; btn.style.border = 'none'; btn.style.borderRadius = '4px'; btn.style.cursor = 'pointer'; btn.style.flex = '1'; btn.addEventListener('click', onClick); return btn; }; const downloadButton = createButton('下载', '#e74c3c', downloadAllHighlights); const clearButton = createButton('清空', '#e74c3c', clearAllHighlights); const toggleButton = createButton('隐藏', '#3498db', () => { if (isSidebarVisible) { sidebar.style.display = 'none'; showSidebarButton.style.display = 'block'; isSidebarVisible = false; adjustBodyMargin(); } }); // 将按钮添加到按钮容器 buttonContainer.appendChild(toggleButton); buttonContainer.appendChild(downloadButton); buttonContainer.appendChild(clearButton); sidebarHeader.appendChild(headerTitle); sidebarHeader.appendChild(buttonContainer); const sidebarContent = document.createElement('div'); sidebarContent.id = 'highlight-sidebar-content'; sidebarContent.style.flexGrow = '1'; sidebarContent.style.padding = '10px'; // 为内容添加内边距 sidebar.appendChild(sidebarHeader); sidebar.appendChild(sidebarContent); // 创建“显示”按钮 const showSidebarButton = document.createElement('button'); showSidebarButton.textContent = ' 显示 '; showSidebarButton.style.position = 'fixed'; showSidebarButton.style.top = 'calc(5em + 10px)'; // 向下移动 5em showSidebarButton.style.right = '10px'; showSidebarButton.style.padding = '5px'; showSidebarButton.style.backgroundColor = '#3498db'; showSidebarButton.style.color = '#fff'; showSidebarButton.style.border = 'none'; showSidebarButton.style.borderRadius = '4px'; showSidebarButton.style.cursor = 'pointer'; showSidebarButton.style.zIndex = '10000'; showSidebarButton.addEventListener('click', () => { sidebar.style.display = 'block'; showSidebarButton.style.display = 'none'; isSidebarVisible = true; adjustBodyMargin(); }); // 实现上下拖动功能 let isDragging = false; let startY; let startTop; showSidebarButton.addEventListener('mousedown', (e) => { isDragging = true; startY = e.clientY; startTop = parseInt(window.getComputedStyle(showSidebarButton).top, 10); document.addEventListener('mousemove', onDrag); document.addEventListener('mouseup', onStopDrag); }); function onDrag(e) { if (!isDragging) return; let deltaY = e.clientY - startY; let newTop = startTop + deltaY; // 限制按钮在视口内 newTop = Math.max(0, Math.min(window.innerHeight - showSidebarButton.offsetHeight, newTop)); showSidebarButton.style.top = newTop + 'px'; } function onStopDrag(e) { isDragging = false; document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', onStopDrag); } document.body.appendChild(sidebar); document.body.appendChild(showSidebarButton); // 检查是否有高亮内容 const centralizedHighlights = JSON.parse(GM_getValue(STORAGE_KEY, '[]')); if (centralizedHighlights.length > 0) { // 如果有高亮内容,显示侧边栏并隐藏“显示”按钮 sidebar.style.display = 'block'; showSidebarButton.style.display = 'none'; isSidebarVisible = true; updateSidebar(centralizedHighlights); // 确保侧边栏内容正确显示 } else { // 如果没有高亮内容,保持侧边栏隐藏,显示“显示”按钮 sidebar.style.display = 'none'; showSidebarButton.style.display = 'block'; isSidebarVisible = false; } // 调整页面内容,防止与侧边栏重叠 adjustBodyMargin(); // 监听侧边栏的调整大小事件,以调整 body 的边距 sidebar.addEventListener('mousemove', adjustBodyMargin); sidebar.addEventListener('mouseup', adjustBodyMargin); } // 调整 body 的右边距,防止内容被侧边栏遮挡 function adjustBodyMargin() { const sidebar = document.getElementById('highlight-sidebar'); if (sidebar && sidebar.style.display !== 'none') { document.body.style.marginRight = `${sidebar.offsetWidth}px`; } else { document.body.style.marginRight = '0'; } } // 下载所有高亮内容为 .txt 文件 function downloadAllHighlights() { const centralizedHighlights = JSON.parse(GM_getValue(STORAGE_KEY, '[]')); if (centralizedHighlights.length === 0) { alert('没有任何高亮内容。'); return; } const highlights = centralizedHighlights.map(item => `${item.html}`); const date = new Date().toISOString().slice(0, 10); const content = `#方案支持:学习骇客(xxhk.org)\n#separator:Tab\n#html:true\n\n${highlights.join('\n\n')}`; const blob = new Blob([content], { type: 'text/plain' }); const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = `Highlight2Anki_${date}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(blobUrl); } // 清空所有高亮内容 function clearAllHighlights() { GM_setValue(STORAGE_KEY, JSON.stringify([])); const sidebarContent = document.getElementById('highlight-sidebar-content'); if (sidebarContent) { sidebarContent.innerHTML = ''; } currentHighlights.clear(); // 清除当前高亮 // 从页面中移除所有高亮 // 移除 hlak-sentence 标签 const hlakSentenceElements = document.querySelectorAll('hlak-sentence'); hlakSentenceElements.forEach(elem => { const parent = elem.parentNode; while (elem.firstChild) { parent.insertBefore(elem.firstChild, elem); } parent.removeChild(elem); parent.normalize(); // 合并相邻的文本节点 }); // 移除 hlak-phrase 标签 const hlakPhraseElements = document.querySelectorAll('hlak-phrase'); hlakPhraseElements.forEach(elem => { const parent = elem.parentNode; while (elem.firstChild) { parent.insertBefore(elem.firstChild, elem); } parent.removeChild(elem); parent.normalize(); // 合并相邻的文本节点 }); // 隐藏侧边栏并显示“显示”按钮 const sidebar = document.getElementById('highlight-sidebar'); const showSidebarButton = document.querySelector('button[style*="显示"]'); if (sidebar && showSidebarButton) { sidebar.style.display = 'none'; showSidebarButton.style.display = 'block'; isSidebarVisible = false; adjustBodyMargin(); } } // 新增的函数:提取包含选中内容的句子,并在选中内容上插入 <hlak-phrase></hlak-phrase> 标签 function extractSentenceWithSelection(event) { const selection = window.getSelection(); if (selection.rangeCount === 0) return; const range = selection.getRangeAt(0); // 扩展范围到句子 let sentenceRange = expandRangeToSentence(range); // 获取句子的文本 let sentenceText = sentenceRange.toString(); // 替换句子中的断行符为空格 sentenceText = sentenceText.replace(/[\r\n]+/g, ' '); // 获取选中的文本 let selectedText = selection.toString(); // 转义特殊的正则表达式字符 let escapedSelectedText = selectedText.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); // 用<u></u>标签包裹选中的文本 let regex = new RegExp(escapedSelectedText, 'g'); let highlightedSentence = sentenceText.replace(regex, '<u>' + selectedText + '</u>'); // 添加页面来源超链接 highlightedSentence += ` <a href="${window.location.href}" target="_blank">*</a>`; // 创建高亮对象并保存 let highlight = { id: generateHighlightId(), html: highlightedSentence, pageTitle: Htitle, pageURL: window.location.href, note: '', }; let centralizedHighlights = JSON.parse(GM_getValue(STORAGE_KEY, '[]')); centralizedHighlights.push(highlight); GM_setValue(STORAGE_KEY, JSON.stringify(centralizedHighlights)); currentHighlights.add(highlight.id); updateSidebar(centralizedHighlights); // 在页面中将选中的内容用 <hlak-phrase></hlak-phrase> 包裹 let hlakPhrase = document.createElement('hlak-phrase'); hlakPhrase.setAttribute('data-highlight-id', highlight.id); try { range.surroundContents(hlakPhrase); } catch (e) { // 如果无法使用 surroundContents,使用替代方法 hlakPhrase.appendChild(range.extractContents()); range.insertNode(hlakPhrase); } // 添加样式类 hlakPhrase.classList.add('hlak-phrase-highlight'); // 清除选区 selection.removeAllRanges(); } // 扩展范围到句子的函数 function expandRangeToSentence(range) { let startNode = range.startContainer; let endNode = range.endContainer; let newRange = range.cloneRange(); // 向前扩展到句子开头 let startOffset = newRange.startOffset; let startText = startNode.textContent; while (startOffset > 0) { startOffset--; let char = startText.charAt(startOffset); newRange.setStart(startNode, startOffset); if (/[.!?。!?]/.test(char)) { // 检查标点后面的字符是否为大写字母或换行符 let nextCharIndex = startOffset + 1; if (nextCharIndex < startText.length) { let nextChar = startText.charAt(nextCharIndex); if (nextChar && (nextChar.match(/[A-Z]/) || /[\r\n]/.test(nextChar))) { newRange.setStart(startNode, nextCharIndex); break; } else { continue; // 继续向前搜索 } } else { // 已经到达文本结尾 newRange.setStart(startNode, nextCharIndex); break; } } else if (startOffset === 0) { newRange.setStart(startNode, startOffset); } } // 向后扩展到句子结尾 let endOffset = newRange.endOffset; let endText = endNode.textContent; let endTextLength = endText.length; while (endOffset < endTextLength) { let char = endText.charAt(endOffset); newRange.setEnd(endNode, endOffset + 1); endOffset++; if (/[.!?。!?]/.test(char)) { // 检查标点后面的字符是否为大写字母、换行符或文本结束 if (endOffset < endTextLength) { let nextChar = endText.charAt(endOffset); if (nextChar && (nextChar.match(/[A-Z]/) || /[\r\n]/.test(nextChar))) { break; } else { continue; // 继续向后搜索 } } else { // 已经到达文本结尾 break; } } } return newRange; } // 添加自定义样式 function addCustomStyles() { const style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = ` hlak-sentence { background-color: rgba(255, 255, 0, 0.5); /* 荧光黄色,50% 透明度 */ padding: 2px 4px; margin: 0 2px; border-radius: 4px; } hlak-phrase { background-color: rgba(0, 255, 0, 0.5); /* 荧光绿色,50% 透明度 */ padding: 2px 4px; margin: 0 2px; border-radius: 4px; } .hlak-sentence-highlight { background-color: rgba(255, 255, 0, 0.5); /* 保持高亮效果 */ } .hlak-phrase-highlight { background-color: rgba(0, 255, 0, 0.5); /* 保持高亮效果 */ } `; document.head.appendChild(style); } // 处理文本选择 function handleSelection(event) { if (highlightTimeout) clearTimeout(highlightTimeout); highlightTimeout = setTimeout(() => { const selection = window.getSelection(); if ((selection && selection.rangeCount > 0 && selection.toString().length > 0) || ((event.target.tagName === 'IMG' || hasBackgroundImage(event.target)) && event.type === 'mouseup')) { // 移除与仅按住 Ctrl 的相关代码 /* if (event.ctrlKey && !event.altKey) { extractSentenceWithSelection(event); } else */ if (event.altKey) { const isCtrlPressed = event.ctrlKey; const showPrompt = isCtrlPressed; // 如果同时按下 Ctrl 键,显示输入框 highlightSelection(showPrompt, event); // 根据 Ctrl 键决定是否显示输入框 } else { // 未按下 Alt 或 Ctrl 键,不执行任何操作 return; } } }, 200); } // 高亮选中的文字或图片,并可添加批注 function highlightSelection(showPrompt = false, event) { if (!isHighlightEnabled) return; const selection = window.getSelection(); // 检查是否点击了图片,且没有选中文本 if (selection.isCollapsed && (event.target.tagName === 'IMG' || hasBackgroundImage(event.target))) { const targetElement = event.target; const highlightId = generateHighlightId(); // 为图片添加高亮样式,而不替换元素 targetElement.setAttribute('data-highlight-id', highlightId); if (showPrompt) { const rect = targetElement.getBoundingClientRect(); showCustomInputBox(rect, targetElement); } else { saveHighlights(true, '', targetElement); } } // 如果选中了文本或文本加图片 else if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); // 创建一个 hlak-sentence 元素包裹选中的内容 const hlakSentence = document.createElement('hlak-sentence'); hlakSentence.setAttribute('data-highlight-id', generateHighlightId()); try { range.surroundContents(hlakSentence); } catch (e) { // 如果无法使用 surroundContents,使用替代方法 hlakSentence.appendChild(range.extractContents()); range.insertNode(hlakSentence); } // 添加样式类 hlakSentence.classList.add('hlak-sentence-highlight'); // 如果需要显示输入框 if (showPrompt) { const rect = hlakSentence.getBoundingClientRect(); showCustomInputBox(rect, hlakSentence); } else { saveHighlights(true, '', hlakSentence); } selection.removeAllRanges(); // 清空选区 } } // 添加事件监听器 document.addEventListener('mouseup', (event) => { handleSelection(event); }); // 调整窗口大小时,调整 body 的边距 window.addEventListener('resize', adjustBodyMargin); // 在页面加载时创建侧边栏并添加样式 window.addEventListener('load', () => { createSidebar(); addCustomStyles(); }); })();