Skip to content

Commit 59d7464

Browse files
committed
Fix highlight errors
1 parent 56e037f commit 59d7464

5 files changed

Lines changed: 132 additions & 25 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ jobs:
1919
- name: Prepare Chrome build
2020
run: |
2121
cp manifest/manifest.chrome.json manifest.json
22-
zip -r marklet-chrome.zip . -x "manifest/manifest.*.json"
22+
zip -r marklet-chrome.zip . -x "manifest/*" ".git/*" ".github/*" "LICENSE"
2323
2424
- name: Prepare Firefox build
2525
run: |
2626
cp manifest/manifest.firefox.json manifest.json
27-
zip -r marklet-firefox.zip . -x "manifest/manifest.*.json" "*.zip"
27+
zip -r marklet-firefox.zip . -x "manifest/*" "*.zip" ".git/*" ".github/*" "LICENSE"
2828
2929
- name: Create Github Release
3030
uses: softprops/action-gh-release@v2

manifest/manifest.chrome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "Marklet",
4-
"version": "1.0",
4+
"version": "1.2",
55
"short_name": "Save and revisit exact text from any webpage. Highlight, bookmark, and instantly jump back to what matters.",
66
"description": "Save and revisit exact text from any webpage. Highlight, bookmark, and instantly jump back to what matters.",
77
"action": {

manifest/manifest.firefox.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 2,
33
"name": "Marklet",
4-
"version": "1.0",
4+
"version": "1.2",
55
"description": "Run bookmarklets easily",
66
"permissions": [
77
"activeTab",
@@ -10,15 +10,15 @@
1010
"browser_action": {
1111
"default_title": "Run Marklet"
1212
},
13-
"background": {
14-
"scripts": [
15-
"background.js"
16-
]
17-
},
18-
"applications": {
13+
"browser_specific_settings": {
1914
"gecko": {
2015
"id": "marklet@example.com",
21-
"strict_min_version": "109.0"
16+
"strict_min_version": "109.0",
17+
"data_collection_permissions": {
18+
"required": [
19+
"none"
20+
]
21+
}
2222
}
2323
}
2424
}

popup/popup.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,13 @@ function jumpTo(bm) {
201201
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
202202
const tab = tabs[0];
203203
if (tab && tab.url.startsWith(bm.url.split("#")[0])) {
204-
chrome.tabs.sendMessage(tab.id, { action: "jumpToBookmark", text: bm.text });
205-
showToast("↗ Jumped to bookmark");
204+
chrome.tabs.sendMessage(tab.id, { action: "jumpToBookmark", text: bm.text }, (response) => {
205+
if (chrome.runtime.lastError || !response?.found) {
206+
showToast("Could not find that text on this page");
207+
return;
208+
}
209+
showToast("↗ Jumped to bookmark");
210+
});
206211

207212
} else {
208213
chrome.tabs.update(tab.id, { url: bm.url }, () => {
@@ -212,9 +217,15 @@ function jumpTo(bm) {
212217

213218
chrome.scripting.executeScript({
214219
target: { tabId: tab.id },
215-
files: ["content.js"]
220+
files: ["scripts/context.js"]
216221
}, () => {
217-
chrome.tabs.sendMessage(tabId, { action: "jumpToBookmark", text: bm.text });
222+
chrome.tabs.sendMessage(tabId, { action: "jumpToBookmark", text: bm.text }, (response) => {
223+
if (chrome.runtime.lastError || !response?.found) {
224+
showToast("Opened page, but text was not found");
225+
return;
226+
}
227+
showToast("↗ Jumped to bookmark");
228+
});
218229
});
219230
}
220231
});

scripts/context.js

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,134 @@
11
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
22
if (message.action === "jumpToBookmark") {
3-
findAndScrollToText(message.text);
4-
sendResponse({ found: true });
3+
const found = findAndScrollToText(message.text);
4+
sendResponse({ found });
55
}
66
});
77

88

99
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+
1019
const walker = document.createTreeWalker(
1120
document.body,
1221
NodeFilter.SHOW_TEXT
1322
);
1423

1524
let node;
1625
while ((node = walker.nextNode())) {
17-
const index = node.textContent.indexOf(searchText);
26+
const rawText = node.textContent || "";
27+
const index = rawText.indexOf(searchText);
1828

1929
if (index !== -1) {
2030
const range = document.createRange();
2131
range.setStart(node, index);
2232
range.setEnd(node, index + searchText.length);
33+
scrollAndHighlight(range);
34+
return true;
35+
}
36+
}
2337

24-
window.scrollTo({
25-
top: range.getBoundingClientRect().top + window.scrollY - 120,
26-
behavior: "smooth"
27-
});
38+
return false;
39+
}
2840

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+
}
3048

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;
32104
}
33105
}
34106

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;
36132
}
37133

38134
function highlightRange(range) {

0 commit comments

Comments
 (0)