Un beau jour, je cherchais comment mettre en forme et enregistrer facilement une conversation avec ChatGPT et je suis tombé sur un concept qui sentait le vieux de loin.
J'étais pas préparé au niveau de vieux de ce truc ancien des temps reculés où l'Eurodance était encore à la mode.
Vous vous souvenez des URL à base de javascript: void()? Si vous êtes un dinosaure du web vous en avez déjà vu par-ci par-là, si vous êtes une personne normale... Ben non mais alors je sais pas ce que vous fabriquez sur ce blog.
Cette technique permettait de donner un attribut href qui ne fait rien du tout à un lien. C'était utile pour... Des trucs. Je sais plus en fait.
C'est possible de bourrer du JavaScript générique dans l'attribut href. Par exemple:
<a href="javascript: void(alert('A que coucou'))">Clique moi</a>
Ce qui donne ceci:
L'origine de cette fourberie serait quelqu'un chez Netscape qui s'est dit que ce serait une super top idée de pouvoir "ouvrir du JavaScript" (??) comme URL et euh... De cette idée découlent des conséquences... Qui mèneront au sujet de cet article.
Attends, t'as dit Netscape? Ben ouais! Si tu sais pas keske c'est Netscape, c'est le Chrome des années 90 sans les brols de Google.
Il y avait aussi son frère maléfique Internet Explorer 6 mais sa lignée s'est éteinte (y a pas tellement longtemps lul).
Bon, d'accord, mais les bookmarklets, c'est quoi alors?
J'y viens, j'y viens!
Si c'est possible de caser du JavaScript dans une URL, et qu'on peut ajouter une URL aux favoris/signets/marque-pages, alors, en toute logique déductive sans faille aucune, il devient possible d'enregistrer du JavaScript en tant que favoris/signet/marque-pages.
Relisez ce paragraphe trois ou quatre fois, ça vaut le coup, promis.
Reprenons mon bête lien qui affiche un message fixe:
Clique-droit sur moi et enregistre moi comme marque-page
Par exemple sur mon Firefox ça ressemble à ça (c'est en anglais, désolé):

Et voilà, vous pouvez ACTIONNER ce fabuleux morceau de code depuis vos marque-pages, et depuis n'importe où sur les Internets. Allez-y, essayez!
Alors, alors?? Je vous avais dit qu'il y aurait un avant et un après cet article. Je l'avais peut-être pas dit. Mais je l'ai pensé très fort.
Et ce n'est que le début. Mon script il affiche juste une boite de dialogue alors qu'on peut accomplir à peu près tout ce qui est possible en JavaScript, y compris parcourir voire modifier la page en cours ou créer une toute nouvelle page avec des infos glanées sur l'autre.
Tu peux aussi poser une question et laisse ce bookmarklet te dire ton avenir:
Mets moi dans tes marques-pages et pose moi des questions
A l'époque de Netscape c'était courant d'utiliser window.open() pour créer du contenu dans une nouvelle fenêtre. Puis il y a eu l'époque des pop-ups parasites et ça a totalement ruiné cette fonctionnalité d'ouverture dynamique de fenêtre pour tout le monde et pour toujours. C'est du coup moyennement fiable de nos jours.
Je proposerais plutôt d'ajouter un élément modal, par exemple.
Pour vous prouver que je mens pas sur l'origine du JavaScript-dans-les-marque-pages, regardez-moi cette page archivée quelque part autour de 1997 qui explique en 15 lignes ce que je viens de faire en 50000.
Les gens ils font quoi avec les bookmarklets?
Je sais pas trop qui utilise les bookmarklets vu que j'ignorais leur exitence jusqu'à relativement récemment.
Le même résultat peut être obtenu en collant du code dans la console développeur du navigateur comme je l'avais montré dans un autre article:
Si le sujet vous titille plus qu'il ne devrait, je vous conseille de consulter ces deux liens (en anglais) avec d'autres exemples concrets:
- www.freecodecamp.org/news/what-are-bookmarklets
- https://www.hongkiat.com/blog/100-useful-bookmarklets-for-better-productivity-ultimate-list
Ma propre petite liste de cas d'utilisations et exemples récurents:
- Compter le nombre de mots sur une page. Waw, ça transpire l'utilité publique majeure.
- Lister les scripts chargés sur la page. Ben ouais, pourquoi pas? Voir exemple plus loin. Je suis d'accord que c'est assez peu utile.
- Lister les images présentes sur la page, que ça soit en tag image, en arrière plan, etc. Voir mon exemple plus loin. C'est personnellement quelque chose qui pourrait m'être utile de temps à autre.
- Balancer une page dans un traducteur. Les navigateurs offrent ça de base de nos jours, c'est donc totalement inutile.
- Calculer un gros hash de tout ou une partie de la page, mettre ça dans le localStorage pour pouvoir le comparer plus tard et vérifier si le contenu concerné a changé depuis la dernière fois — Je viens d'inventer ce cas d'utilisation extrêmement élaboré, c'est cadeau si vous souhaitez l'implémenter parce que moi j'ai pas le temps.
- Enregistrer le texte de la page en fichier texte. Suffit de copier coller le texte dans un autre programme. Plutôt inutile.
- Virer des parties de la page qu'on ne veut pas voir. Par ex. ces horribles pubs de bas de page qui avec les pires titres putaclick de l'univers. Par contre ça doit être ciblé spécifiquement site par site donc potentiellement beaucoup de boulot.
- Remplacer la police de caractères par Comic Sans MS partout sur la page. Utile pour une mono-blague nulle à faire à un collègue — Il y a d'ailleurs d'autres blagues possibles du même acabit à base de tout transformer en tag marquee, changer l'arrière plan en rose (attention ce lien change réellement l'arrière-plan en rose, ajoutez-le à vos marque-pages!), ... Qu'est-ce qu'on se marre. Haha. Hihi.
- Créer un mode sombre du pauvre sur un site qui n'a qu'un mode clair en remplaçant des couleurs d'arrière-plan et de texte par-ci par-là. Risque d'être un peu foireux.
Comment créer son propre bookmarklet?
Il s'agit de JavaScript mais avec quelques détails hérités de la grande époque.
NB: Cette section suppose que vous avez certaines connaissances en programmation et en JavaScript, par exemple que vous savez ce qu'est une fonction et ce que signifie "retourner une valeur".
Premièrement, renvoyer une valeur depuis une URL "JavaScript" — que ça soit par le biais d'une expression simple ou d'un retour de fonction — remplace tout le contenu de la page courante avec ce qui est retourné.
Exemple de code ici pour un lien piégé, qui contient une expression:
<a href="javascript: 'Juste du texte ici'">NE ME CLIQUEZ PAS</a>
Ce lien, si vous cliquez dessus sans utiliser une action spéciale pour ouvrir dans un nouvel onglet comme un petit malin, il va effacer toute la page et la remplacer par la valeur de retour de l'expression JavaScript et il faudra rafraichir la page pour retrouver contenu original.
Vous voilà prévenus!
Ceci implique d'éviter de retourner des valeurs depuis l'expression JavaScript utilisée comme URL.
Une des techniques principales consiste à déclarer une fonction, bien s'assurer de ne rien retourner dans cette fonction, et appeler immédiatement la dite fonction je dis beaucoup le mot fonction.
Ce concept s'appelle une IIFE en JavaScript, couramment utilisé pour éviter de définir des variables et fonctions globales dans un script sur une page et ainsi avoir des collisions de noms possibles mais on s'égare. Qui a dit comme d'habitude?
Un autre plan implique le méconnu mot clé "void" qui était présent dans les premiers exemples que présentés.
Le mot clé void annule la valeur de retour d'une expression qu'il contient entre ses parenthèses (bien qu'en fait les parenthèses soient optionnelles, lol JavaScript lol), en cela qu'il la transforme en undefined, parce qu'une expression ou une fonction renvoie toujours secrètement undefined si rien n'est retourné explicitement.
Exemple:
let a = 2 + 2; // a = 4
let b = void(2 + 2); // b = undefined
Pour les bookmarklets on pourrait tout entourer de void et euh... Ben voilà.
L'IIFE est plus courante (pour son bonus de ne pas toucher au contexte global) tant qu'elle ne retourne rien (c'est à dire qu'elle retourne undefined).
C'est aussi pour ça qu'on ne va pas utiliser de fonction fléchée sur une seule ligne parce qu'elle renvoie implicitement cette ligne. Une IIFE fléchée pour bookmarklets (ou URL JavaScript) ressemblerait plutôt à ceci:
(() => { /* Code ici */ })()
Dernièrement, pour que la bookmarklet soit opérationnelle, elle doit être sur une seule ligne à la manière d'un très long bout de code moche.
Ce qui n'empêche pas de travailler proprement dans un éditeur quelconque avec du code aéré sur plusieurs lignes et peut-être même sans points-virgules comme j'en ai l'habitude, il faudra juste passer ce code dans une moulinette afin d'obtenir une seule ligne (qui fonctionne toujours si possible).
Certains "minifieurs" (minificateurs?) de code le font, d'autres laissent certains retours à la ligne que je retire avec une bonne vieille commande "tr" dans mon terminal parce que je suis un genre de magicien de Linux, mais chercher/remplacer "\n" ou "\r\n" devrait aussi fonctionner dans un éditeur comme Notepad++ par exemple.
Je présenterai une solution dans l'exemple concret qui suit.
Création d'un bookmarklet: isoler et afficher toutes les images de la page en cours
Je voudrais pouvoir extraire/isoler toutes les images qui seraient présentes sur une page choisie, qu'il s'agisse de classiques tags img comme d'images placées en arrière-plan par des règles CSS.
Tant qu'on y est je copie aussi tous les tags svg à l'identique, et je cherche les plus obscurs tags source afin de trouver les images alternatives ou ce qui est utilisé dans le plus obscur tag picture.
Je comprends pas tout au tag source pour être honnête et j'ai pas le temps de comprendre et mon code va pas vraiment être parfait. Du tout. A vous de me perfectionner ça 🩲💨.
const urls = new Set()
const relToAbs = (url) => {
// Add location.origin if not present yet.
// I discovered the existence of URLs starting with
// "//", it just means "use the current protocol".
// Learn something new everyday.
let protoReg = new RegExp(`^(https?:)?//`)
if (!protoReg.test(url)) {
return `${location.origin}/${url.replace(/^\//, "")}`
}
return url
}
const addAttrToUrls = (tagName, attr) => {
const tags = document.querySelectorAll(tagName)
for (e of tags) {
const src = e.getAttribute(attr)
src && urls.add(relToAbs(src))
}
}
addAttrToUrls("img", "src")
// srcset has a special format with multiple comma
// separated urls + image size.
const sourceTags = document.querySelectorAll("source")
for (t of sourceTags) {
const src = t.getAttribute("srcset")
if (src) {
src.split(",").map(s => s.split(" ")[0].trim())
.forEach(s => urls.add(relToAbs(s)))
}
}
document.querySelectorAll("body, body > *").forEach(e => {
const bgImg = getComputedStyle(e).backgroundImage
// Extract what's in "url()" or ditch the result.
bgImg && [...bgImg.matchAll(/url\((.*?)\)/g)]
.map(b => b[1].replaceAll("\"", "").replaceAll("'", ""))
.forEach(b => urls.add(relToAbs(b)))
})
const styles = `<style>
body {display: flex; flex-direction: column; margin: 0}
section {background: #fff; border-bottom: 4px solid deeppink; display: flex; flex-direction: column}
img,svg {display: inline}
img {max-width: 100%; width: min-content; min-width: 80px}
a {color: #000; display: block; margin: 10px; font-size: 1rem}
a:hover {color: #555; text-decoration: none}
</style>`
// Need to get refs to svgs before destroying body content:
const svgs = Array.from(document.querySelectorAll("svg"))
document.body.className = ""
document.head.innerHTML = `<title>${document.title}</title>`
document.body.innerHTML = `${styles}
${Array.from(urls).map(u => `<section><a href="${u}">${u}</a><img src="${u}"></section>`).join("\n")}`
const svgSections = svgs.map(svg => {
const sec = document.createElement("section")
sec.appendChild(svg)
return sec
})
document.body.append(...svgSections)
Okay... Il se passe quoi dans cette horreur?
J'utilise un objet Set pour stocker les URLs de sorte que je n'ajoute pas la même deux fois. J'aurais pu utiliser un bête array et écrire une fonction d'ajout spécial qui vérifie si l'URL existe déjà dedans et en fait ça aurait peut-être même été plus rapide en terme de performances mais cet objet Set existe, autant l'utiliser nessepas.
Je remplace toutes les URL relatives par des absolues en récupérant le location.origin de la page en cours.
Mon plan pour afficher le résultat est super sauvage et consiste à détruire tout le contenu de la page en cours avec document.body.innerHTML = "", ce qui en fait ne suffit pas tout à fait à effacer tout le tableau parce qu'il reste des déclarations de style et des scripts dans head.
C'est pas grave je détruis aussi totalement ce qui est dans head. Je remets quand même le titre de la page parce que c'est la moindre des choses.
J'ajoute mes propres styles dégeulasses sur la nouvelle page presque-vierge suivi des images qui sont chaque fois précédées d'un petit-lien-qui-va-bien.
Le code peut-être facilement testé en le collant dans la console JavaScript.
Il y a moyen d'aller beaucoup plus loin en permettant de trier par taille, type, etc.
Ajouter un bouton pour changer l'arrière-plan de blanc à noir et vice-versa peut-être utile aussi pour voir certaines images qui sont en réalité toutes blanches ou toutes noires. Un sacré projet dites-moi.
Il ne reste plus qu'à transformer ce code en quelque chose qui peut-être enregistré en marque-page.
Je rappelle qu'il nous faudrait un outil qui retire les retours à la ligne aussi sinon on va devoir le faire à la main après.
Cet outil en ligne fonctionne très bien pour ça: https://minify-js.com
Si votre code est destiné à une production industrielle il y a des librairies JavaScript pour minifier avec NodeJS. Comme je suis sympa je vous montre un exemple (implique d'être sous Mac ou Linux ou WSL parce que j'utilise la commande tr pour virer les retours à la ligne):
npx minify monsuperscript.js | tr -d "\n"
Ceci dit, on est pas obligés d'utiliser la ligne de commande, Avec l'outil en ligne précité j'obtiens ceci:
const urls=new Set,relToAbs=e=>new RegExp("^(https?:)?//").test(e)?e:`${location.origin}/${e.replace(/^\//,"")}`,addAttrToUrls=(t,o)=>{const n=document.querySelectorAll(t);for(e of n){const t=e.getAttribute(o);t&&urls.add(relToAbs(t))}};addAttrToUrls("img","src");const sourceTags=document.querySelectorAll("source");for(t of sourceTags){const e=t.getAttribute("srcset");e&&e.split(",").map((e=>e.split(" ")[0].trim())).forEach((e=>urls.add(relToAbs(e))))}document.querySelectorAll("body, body > *").forEach((e=>{const t=getComputedStyle(e).backgroundImage;t&&[...t.matchAll(/url\((.*?)\)/g)].map((e=>e[1].replaceAll('"',"").replaceAll("'",""))).forEach((e=>urls.add(relToAbs(e))))}));const styles="<style>\nbody {display: flex; flex-direction: column; margin: 0}\nsection {background: #fff; border-bottom: 4px solid deeppink; display: flex; flex-direction: column}\nimg,svg {display: inline}\nimg {max-width: 100%; width: min-content; min-width: 80px}\na {color: #000; display: block; margin: 10px; font-size: 1rem}\na:hover {color: #555; text-decoration: none}\n</style>",svgs=Array.from(document.querySelectorAll("svg"));document.body.className="",document.head.innerHTML=`<title>${document.title}</title>`,document.body.innerHTML=`${styles}\n\t${Array.from(urls).map((e=>`<section><a href="${e}">${e}</a><img src="${e}"></section>`)).join("\n")}`;const svgSections=svgs.map((e=>{const t=document.createElement("section");return t.appendChild(e),t}));document.body.append(...svgSections);
Qui fonctionne déjà comme bookmarklet.
Pour être complets et consciencieux nous allons tout de même en faire une IIFE en l'entourant de (() => {})();, ce qui donne (j'ai ajouté le petit "javascript:" devant pour pouvoir en faire un bookmarklet):
javascript:(()=>{const urls=new Set,relToAbs=e=>new RegExp("^(https?:)?//").test(e)?e:`${location.origin}/${e.replace(/^\//,"")}`,addAttrToUrls=(t,o)=>{const n=document.querySelectorAll(t);for(e of n){const t=e.getAttribute(o);t&&urls.add(relToAbs(t))}};addAttrToUrls("img","src");const sourceTags=document.querySelectorAll("source");for(t of sourceTags){const e=t.getAttribute("srcset");e&&e.split(",").map((e=>e.split(" ")[0].trim())).forEach((e=>urls.add(relToAbs(e))))}document.querySelectorAll("body, body > *").forEach((e=>{const t=getComputedStyle(e).backgroundImage;t&&[...t.matchAll(/url\((.*?)\)/g)].map((e=>e[1].replaceAll('"',"").replaceAll("'",""))).forEach((e=>urls.add(relToAbs(e))))}));const styles="<style>\nbody {display: flex; flex-direction: column; margin: 0}\nsection {background: #fff; border-bottom: 4px solid deeppink; display: flex; flex-direction: column}\nimg,svg {display: inline}\nimg {max-width: 100%; width: min-content; min-width: 80px}\na {color: #000; display: block; margin: 10px; font-size: 1rem}\na:hover {color: #555; text-decoration: none}\n</style>",svgs=Array.from(document.querySelectorAll("svg"));document.body.className="",document.head.innerHTML=`<title>${document.title}</title>`,document.body.innerHTML=`${styles}\n\t${Array.from(urls).map((e=>`<section><a href="${e}">${e}</a><img src="${e}"></section>`)).join("\n")}`;const svgSections=svgs.map((e=>{const t=document.createElement("section");return t.appendChild(e),t}));document.body.append(...svgSections);})();
Magnifique, je pourrais vous le mettre dans l'attribut href d'un lien pour pouvoir facilement l'ajouter aux marque-pages, ou on peut de suite l'ajouter nous mêmes depuis le bidule de gestion des marque-pages (parce que cet extrait de code est horrible à échapper pour le mettre en lien et j'ai pas que ça à faire):
Reste plus qu'à l'appeler depuis n'importe quelle page, et BOUM! Toutes les images sont là. Trop bien.
Les vrais amateurs de bookmarklets, s'ils sont pas déjà tous morts, utilisent la barre de favoris c'est à dire qu'ils ont une barre d'outils spéciale en dessous de la barre d'adresse du navigateurs avec tous leurs supers bookmarklets. Trop bien.
Autre exemple, différent type d'affichage
Effacer toute la page (et tout head) ça marche mais c'est un peu rude.
Utiliser alert() ça va mais c'est impossible à mettre en forme.
Utiliser window.open() ça va potentiellement déclencher la protection anti pop-ups sauvages du navigateur.
J'avais effleuré plus haut l'idée d'utiliser un modal ajouté dynamiquement à la page.
Voici un exemple qui utilise d'ailleurs le presque-nouveau (c'est pas nouveau du tout) tag dialog et qui liste les tags script présent sur la page (oui c'est moyen utile, désolé).
const modalId = "dkvzInfoModal"
// First check if the modal already exists:
let modalRoot = document.getElementById(modalId)
if (!modalRoot) {
const modal = document.createDocumentFragment()
modalRoot = document.createElement("dialog")
modalRoot.id = modalId
// The dialog element acutally needs little styling to
// be functional.
modalRoot.style.minWidth = "280px"
modal.appendChild(modalRoot)
document.body.prepend(modal)
}
const scripts = Array.from(document.querySelectorAll('script'))
const modalScriptsText = scripts
.map(s => s.src ? `<li>${s.src}</li>`: "<li>Local script</li>")
modalRoot.innerHTML = `
<button aria-label="close" style="float: right">Close</button>
<h3>Scripts src:</h3>
<ol>
${modalScriptsText.join("\n")}
</ol>`
modalRoot.querySelector('button').addEventListener("click", e => {
console.log(e.target.parentElement.close())
})
modalRoot.showModal()
Cette fois-ci je vous le mets en lien: Voir les scripts de cette page, peut-être il y en a un qui est suspect!
Conclusion
Quand on croit qu'on a tout vu du monde fantastique de JavaScript, on tombe sur un vieux carton poussiéreux au fond du grenier et voilà quatre heures perdues à écrire une brève qui est tout sauf brève sur un truc qui n'intéresse probablement personne et sans titre putaclick en plus histoire de bien renvoyer le truc dans les tréfonds les plus obscurs de l'Internet.
I'm bluuue dabedidabedaaaa... Bebadsfgklmjglfozehfdsmf





Commentaires
Il faut JavaScript activé pour écrire des commentaires ici