A somewhat ancient yet pretty cool feature of web browsers are the bookmarklets. These are literally just javascript: code snippets saved as bookmarks – they are like the older and less capable siblings of typical browser extensions and are limited to being run when clicked and only in the context of the page you're currently looking at. Anyway, since I use two such bookmarklets pretty reguraly, I decided to share them with you.
Note that both bookmarklets, as well as any updates to them, are available on my GitHub in the random-stuff repository.
P.S. If you decide to explore other bookmarklets out there, remember that random bookmarklet found on the internet may contain malicious code. In such case executing it might leak the page you're looking at, leak authentication information (session cookies), or even give an attacker interactive control over the page in said tab (which allows them to change settings, and at times e-mails or even the account password). So if you can't security-review a bookmarklet, popular extensions in good standing are a safer choice.
This one is useful is you're dealing with a website which displays images in a weird way that makes it harder to use features like Save image as... or Copy image link. It basically goes through the DOM and finds every <img> tag and notes the URL to the image, as well as every other tag and notes the url(...) in background-image CSS style (if any). And then it re-renders the page displaying only the images and their URLs.
Minified bookmarklet form (readable form is below):
javascript:{const imgs = [];const re = /url\([ \t]*['"`]\x3f([^\)'"`]+)['"`]\x3f[ \t]*\)/;const fnc = function(parent) { Array.from(parent.children).forEach(child => { if (child.tagName === 'IMG') { imgs.push(child.src); } const bg = child.style.backgroundImage; if (bg && bg.toLowerCase().includes("url(")) { const m = bg.match(re); if (m) { imgs.push(m[1]); } else { console.warn("Failed to extract image URL from:", bg); } } fnc(child); });};fnc(document.body);document.body.innerHTML = "";imgs.forEach(img => { const div = document.createElement("DIV"); const p = document.createElement("P"); const a = document.createElement("A"); a.href = img; a.innerText = img; p.appendChild(a); div.appendChild(p); const el = new Image(); el.src = img; div.appendChild(el); document.body.appendChild(div);});}
Readable source code:
const imgs = [];
const re = /url\([ \t]*['"`]\x3f([^\)'"`]+)['"`]\x3f[ \t]*\)/;
const fnc = function(parent) {
Array.from(parent.children).forEach(child => {
if (child.tagName === 'IMG') {
imgs.push(child.src);
}
const bg = child.style.backgroundImage;
if (bg && bg.toLowerCase().includes("url(")) {
const m = bg.match(re);
if (m) {
imgs.push(m[1]);
} else {
console.warn("Failed to extract image URL from:", bg);
}
}
fnc(child);
});
};
fnc(document.body);
document.body.innerHTML = "";
imgs.forEach(img => {
const div = document.createElement("DIV");
const p = document.createElement("P");
const a = document.createElement("A");
a.href = img;
a.innerText = img;
p.appendChild(a);
div.appendChild(p);
const el = new Image();
el.src = img;
div.appendChild(el);
document.body.appendChild(div);
});
This is something I use if I want the browser to read me a paragraph or two of the text on a website. Initially I thought I would just use an existing extension for this, but then I remembered that browsers actually have text-to-speech built in in form or the SpeechSynthesis API (window.speechSynthesis and friends), so I decided to make a quick one-liner instead. Of course it turned out that in Chrome on Linux only around 200 characters I've read, so I had to add some code (part of which was ChatGPT generated) which creates a list of "sentences" – i.e. words reformated to make 200-or-less character fragments.
Minified bookmarklet form (readable form is below):
javascript:window.slang="en-EN";function processText(e){let t=e.replace(/\n/g," ").replace(/\t/g," ");for(;t.includes(" ");)t=t.replace(/ /g," ");return createSentences(t.split(" "))}function createSentences(e){let t=[],n="";for(;e.length>0;){let s=e.shift();n.length+s.length+1>200?(e.unshift(s),t.push(n.trim()),n=""):(n+=" "+s).length>50&&s.endsWith(".")&&(t.push(n.trim()),n="")}return n.trim().length>0&&t.push(n.trim()),t}function speakNext(){if(0==window.sss.length){console.log("THE END");return}let e=window.sss.shift();var t=new SpeechSynthesisUtterance(e);t.lang=window.slang,console.log("speaking:",e),t.onend=function(e){console.log("SpeechSynthesisUtterance.onend"),window.speechSynthesis.cancel(),speakNext()},t.onerror=function(e){console.error("SpeechSynthesisUtterance.onerror",e.error)},window.speechSynthesis.speak(t)}window.speechSynthesis.cancel(),window.sss=processText(window.getSelection().toString()),speakNext();
Readable source code:
window.slang='en-EN'; /* change this to whatever language you need */
function processText(text) {
let s = text.replace(/\n/g, " ").replace(/\t/g, " ");
while (s.includes(" ")) {
s = s.replace(/ /g, " ");
}
return createSentences(s.split(" "));
}
function createSentences(words) {
let sentences = [];
let currentSentence = "";
while (words.length > 0) {
let word = words.shift();
if (currentSentence.length + word.length + 1 > 200) {
words.unshift(word);
sentences.push(currentSentence.trim());
currentSentence = "";
} else {
currentSentence += " " + word;
if (currentSentence.length > 50 && word.endsWith(".")) {
sentences.push(currentSentence.trim());
currentSentence = "";
}
}
}
if (currentSentence.trim().length > 0) {
sentences.push(currentSentence.trim());
}
return sentences;
}
function speakNext() {
if (window.sentences.length == 0) {
console.log('The End.');
return;
}
const s = window.sentences.shift();
var utterance = new SpeechSynthesisUtterance(s);
utterance.lang = window.slang;
console.log('Speaking:', s)
utterance.onend = function(event) {
window.speechSynthesis.cancel();
speakNext()
};
utterance.onerror = function(event) {
console.error('SpeechSynthesisUtterance.onerror', event.error);
};
window.speechSynthesis.speak(utterance);
}
window.speechSynthesis.cancel();
window.sentences = processText(window.getSelection().toString());
speakNext();