|
1 | 1 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { |
2 | 2 | if (message.action === "jumpToBookmark") { |
3 | | - findAndScrollToText(message.text); |
4 | | - sendResponse({ found: true }); |
| 3 | + const found = findAndScrollToText(message.text); |
| 4 | + sendResponse({ found }); |
5 | 5 | } |
6 | 6 | }); |
7 | 7 |
|
8 | 8 |
|
9 | 9 | function findAndScrollToText(searchText) { |
| 10 | + const normalizedSearch = normalizeText(searchText); |
| 11 | + if (!normalizedSearch) return false; |
| 12 | + |
| 13 | + const rangeFromNodes = findRangeAcrossTextNodes(normalizedSearch); |
| 14 | + if (rangeFromNodes) { |
| 15 | + scrollAndHighlight(rangeFromNodes); |
| 16 | + return true; |
| 17 | + } |
| 18 | + |
10 | 19 | const walker = document.createTreeWalker( |
11 | 20 | document.body, |
12 | 21 | NodeFilter.SHOW_TEXT |
13 | 22 | ); |
14 | 23 |
|
15 | 24 | let node; |
16 | 25 | while ((node = walker.nextNode())) { |
17 | | - const index = node.textContent.indexOf(searchText); |
| 26 | + const rawText = node.textContent || ""; |
| 27 | + const index = rawText.indexOf(searchText); |
18 | 28 |
|
19 | 29 | if (index !== -1) { |
20 | 30 | const range = document.createRange(); |
21 | 31 | range.setStart(node, index); |
22 | 32 | range.setEnd(node, index + searchText.length); |
| 33 | + scrollAndHighlight(range); |
| 34 | + return true; |
| 35 | + } |
| 36 | + } |
23 | 37 |
|
24 | | - window.scrollTo({ |
25 | | - top: range.getBoundingClientRect().top + window.scrollY - 120, |
26 | | - behavior: "smooth" |
27 | | - }); |
| 38 | + return false; |
| 39 | +} |
28 | 40 |
|
29 | | - highlightRange(range); |
| 41 | +function scrollAndHighlight(range) { |
| 42 | + window.scrollTo({ |
| 43 | + top: range.getBoundingClientRect().top + window.scrollY - 120, |
| 44 | + behavior: "smooth" |
| 45 | + }); |
| 46 | + highlightRange(range); |
| 47 | +} |
30 | 48 |
|
31 | | - return true; |
| 49 | +function normalizeText(text) { |
| 50 | + return (text || "") |
| 51 | + .replace(/\u00a0/g, " ") |
| 52 | + .replace(/\s+/g, " ") |
| 53 | + .trim() |
| 54 | + .toLowerCase(); |
| 55 | +} |
| 56 | + |
| 57 | +function findRangeAcrossTextNodes(normalizedSearch) { |
| 58 | + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); |
| 59 | + const nodes = []; |
| 60 | + const nodeStarts = []; |
| 61 | + let combined = ""; |
| 62 | + let node; |
| 63 | + |
| 64 | + while ((node = walker.nextNode())) { |
| 65 | + if (!node.textContent || !node.textContent.trim()) continue; |
| 66 | + nodes.push(node); |
| 67 | + nodeStarts.push(combined.length); |
| 68 | + combined += node.textContent + " "; |
| 69 | + } |
| 70 | + |
| 71 | + const normalizedCombined = normalizeText(combined); |
| 72 | + const matchStart = normalizedCombined.indexOf(normalizedSearch); |
| 73 | + if (matchStart === -1) return null; |
| 74 | + |
| 75 | + const mapping = buildNormalizedToRawMap(combined); |
| 76 | + const rawStart = mapping[matchStart]; |
| 77 | + const rawEnd = mapping[matchStart + normalizedSearch.length - 1] + 1; |
| 78 | + if (rawStart == null || rawEnd == null) return null; |
| 79 | + |
| 80 | + const startPos = toNodeOffset(rawStart, nodes, nodeStarts); |
| 81 | + const endPos = toNodeOffset(rawEnd, nodes, nodeStarts); |
| 82 | + if (!startPos || !endPos) return null; |
| 83 | + |
| 84 | + const range = document.createRange(); |
| 85 | + range.setStart(startPos.node, startPos.offset); |
| 86 | + range.setEnd(endPos.node, endPos.offset); |
| 87 | + return range; |
| 88 | +} |
| 89 | + |
| 90 | +function buildNormalizedToRawMap(rawText) { |
| 91 | + const map = []; |
| 92 | + let prevWasSpace = true; |
| 93 | + |
| 94 | + for (let i = 0; i < rawText.length; i++) { |
| 95 | + const ch = rawText[i] === "\u00a0" ? " " : rawText[i]; |
| 96 | + if (/\s/.test(ch)) { |
| 97 | + if (!prevWasSpace) { |
| 98 | + map.push(i); |
| 99 | + prevWasSpace = true; |
| 100 | + } |
| 101 | + } else { |
| 102 | + map.push(i); |
| 103 | + prevWasSpace = false; |
32 | 104 | } |
33 | 105 | } |
34 | 106 |
|
35 | | - return false; |
| 107 | + if (map.length && /\s/.test(rawText[map[map.length - 1]])) { |
| 108 | + map.pop(); |
| 109 | + } |
| 110 | + |
| 111 | + return map; |
| 112 | +} |
| 113 | + |
| 114 | +function toNodeOffset(rawIndex, nodes, nodeStarts) { |
| 115 | + for (let i = 0; i < nodes.length; i++) { |
| 116 | + const start = nodeStarts[i]; |
| 117 | + const end = start + nodes[i].textContent.length; |
| 118 | + if (rawIndex <= end) { |
| 119 | + return { |
| 120 | + node: nodes[i], |
| 121 | + offset: Math.max(0, Math.min(rawIndex - start, nodes[i].textContent.length)) |
| 122 | + }; |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + if (nodes.length > 0) { |
| 127 | + const last = nodes[nodes.length - 1]; |
| 128 | + return { node: last, offset: last.textContent.length }; |
| 129 | + } |
| 130 | + |
| 131 | + return null; |
36 | 132 | } |
37 | 133 |
|
38 | 134 | function highlightRange(range) { |
|
0 commit comments