Skip to content

Instantly share code, notes, and snippets.

@kbauer
Last active June 30, 2023 09:47
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kbauer/e8e38e1ca2479403f2243bbf7ae05faa to your computer and use it in GitHub Desktop.
Save kbauer/e8e38e1ca2479403f2243bbf7ae05faa to your computer and use it in GitHub Desktop.

Reader Mode.js

Add as bookmarklet (see below):

javascript:/* Reader Mode.js */(function(){ const IFWL=['gfycat.com','disqus.com','www.youtube.com',location.host,];const ifbl=new RegExp('derstandard\\.at\\/AdServer\\/');const startupTasks=[disableViewport,];const periodicTasks=[disableStaticFloaters,removeIframes,];function disableStaticFloaters(){for(let e of document.getElementsByTagName('*')){if(window.getComputedStyle(e).position.startsWith('fixed')){e.style.setProperty('position','static','important');}}}function removeIframes(){const iframes=getAllIframesRecursively();for(const e of getAllIframesRecursively().reverse()){try{const doRemove=(!e.src)? false : IFWL.indexOf(new URL(e.src).host)==-1 ? true : e.src.match(ifbl)? true : false;if(doRemove&&e.parentElement){e.parentElement.removeChild(e);}}catch(err){reportError(err,['FAILED TO REMOVE IFRAME',e]);}}window.readerModeRemainingIframes=getAllIframesRecursively();}function disableViewport(){const vp=document.querySelector('meta[name=viewport]');if(vp){document.head.removeChild(vp);}}function reformatContents(){const d=document.getElementById('--reader-mode-wrapper')||document.createElement('div');for(const e of document.getElementsByTagName('*')){if(e.tagName=='ARTICLE'){d.appendChild(e);e.style.maxWidth='100%';e.style.width='100%';}}for(const e of document.body.childNodes){e!==d&&d.appendChild(e);}{d.id='--reader-mode-wrapper';const c=d.style;c.width='';c.maxWidth='50em';c.margin='0 auto';c.textAlign='justify';c.float='none';}document.body.prepend(d);{const s=document.createElement('style');s.innerHTML=[1,2,3,4,5].map(n=>`h${n} { font-weight: bold; font-size: ${1.6 - 0.1*n}rem; margin-top: 1rem; margin-bottom: 0.5rem; border-bottom: 1px solid silver }`).join('\n');document.head.appendChild(s);}window.scrollTo(0,0);}function getAllIframesRecursively(){return recur(document);function recur(doc){let iframes=Array.from(doc.getElementsByTagName('iframe'));for(const e of iframes){try{iframes.push.apply(iframes,recur(e.contentDocument));}catch(err){}}return iframes;}}function reportError(err,msg){msg=Array.from(msg);msg.push('\n',err);console.log.apply(console,msg);window.readerModeLastError=msg;}function execTask(func){try{func();}catch(err){reportError(err,["FAILED TO EXECUTE TASK",func.name]);}}startupTasks.map(execTask);periodicTasks.map(execTask);window.setInterval(function execRepeat(){periodicTasks.map(execTask);},1000);})();undefined;

A bookmarklet for stripping websites of distracting elements, such as iframe-based ads and static floaters.1

It is meant as an ad-hoc adblocker and reading-convience tool, rather than as a full-scale adblocker. Most importantly, it doesn't block any tracking at all, as the tracking has already happened by the time a user may execute this script.

A whitelist const IFWL allows preserving desirable iframes, and is defined in the Settings section of the script. This list is meant to be customized, as needed. Note that currently it can only contain host names, as returned by

javascript:alert(location.host);

Reformatting for an iOS-style "reader view" is contained experimentally, but disabled by default, as it works quite badly right now.

Compiled Form

Created using myjs-compile-bookmarklet.

Note that some browsers may remove the javascript: prefix when pasting URLs, in order to protect novice users from harming themselves by executing malicious javascript code.

For pretty much any device and browser, it should work by

  • Create a new bookmark for an arbitrary webpage. It must be in a location, where it can be opened without leaving the current tab or page.

    • On desktop systems, this is typically a bookmark bar.
    • On some mobile browsers, a bookmark pane can be opened.
    • On some browsers (such as mobile Google chrome) bookmarklets can be accessed without leaving the page exclusively by using the address bar to search for the bookmarklet. In such cases, it is adviseable to name the bookmarklet by some short alias that can be quickly typed on mobile keyboards.
    • On iOS and Android Chrome specifically, using three-character shorthands like qqq works well, and the same shorthand can be used for up to three bookmarklets, due to how the address bar displays search results.
  • Edit the bookmark, and replace its URL by the javascript: line. How this works is strongly browser dependent, and it may not be possible at all with some mobile browsers.

Footnotes

1 A lot of websites contain static elements, that remain at fixed screen positions when scrolling. Often these result in rediculously narrow content panes, so the bookmarklet places them disables the CSS property, that makes them float.

/* Reader Mode.js */
//// ====== SETTINGS ======
//// == Whitelist iframes by host
const IFWL = [
'gfycat.com',
'disqus.com',
'www.youtube.com',
location.host, /* Can no longer include location.host, because some webpages
* have started wrapping ads into location.host-iframes. */
];
//// == Blacklist subset of the iframes by regexp
const ifbl = new RegExp('derstandard\\.at\\/AdServer\\/');
//// == Tasks by execution time.
const startupTasks = [
disableViewport,
// reformatContents,
];
const periodicTasks = [
disableStaticFloaters, // static elements and iframes may be
removeIframes, // added back at runtime -> repeat.
];
//// ====== TASK DEFINITIONS ======
function disableStaticFloaters() {
for(let e of document.getElementsByTagName('*')){
if(window.getComputedStyle(e).position.startsWith('fixed')){
e.style.setProperty('position','static','important');
}
}
}
function removeIframes() {
const iframes = getAllIframesRecursively();
for(const e of getAllIframesRecursively().reverse()){ // nested iframes first.
try{
const doRemove =
/* Some pages wrap ads in src-less iframes. But that
* should now be handled by accessing nested iframes too. */
(!e.src) ? false :
IFWL.indexOf(new URL(e.src).host) == -1 ? true :
e.src.match(ifbl) ? true :
false;
if(doRemove && e.parentElement){
e.parentElement.removeChild(e);
}
} catch(err) {
reportError(err, ['FAILED TO REMOVE IFRAME', e]);
}
}
window.readerModeRemainingIframes = getAllIframesRecursively();
}
function disableViewport() {
const vp = document.querySelector('meta[name=viewport]');
if(vp){ document.head.removeChild(vp); }
}
function reformatContents() {
const d =
document.getElementById('--reader-mode-wrapper')
|| document.createElement('div');
//// Move <article> part to top.
// Written to be extensible by other criteria.
for(const e of document.getElementsByTagName('*')){
if(e.tagName == 'ARTICLE'){
d.appendChild(e);
// Making width fit the `d`-wrapper
e.style.maxWidth = '100%';
e.style.width = '100%';
}
// // Linearize divs
// // TODO: More often than not breaks the view
// else if(e.tagName == 'DIV'){
// e.outerHTML = e.innerHTML;
// }
}
//// Move all remaining elements into ``d`` div.
for(const e of document.body.childNodes){
e!==d && d.appendChild(e);
}
//// Body width: Wrap inside div, and limit width.
{
d.id = '--reader-mode-wrapper';
const c = d.style;
c.width = '';
c.maxWidth = '50em';
c.margin = '0 auto';
c.textAlign = 'justify';
c.float = 'none'; // was required by futurezone
}
document.body.prepend(d);
//// Add fallback style, since Angular styles may get broken.
/* Problem originally encountered on futurezone.at */
{
const s = document.createElement('style');
s.innerHTML = [1,2,3,4,5].map(
n => `h${n} { font-weight: bold; font-size: ${1.6 - 0.1*n}rem; margin-top: 1rem; margin-bottom: 0.5rem; border-bottom: 1px solid silver }`
).join('\n');
document.head.appendChild(s);
}
//// Go to start
// Changing layout and element position may cause unvoluntary
// scrolling to the position where the 'position:fixed'
// elements have ended up; Go to start instead.
window.scrollTo(0,0);
}
//// ====== AUX FUNCTIONS ======
function getAllIframesRecursively() {
/** Return all iframes in current document, and all iframes
* in accessible iframes. */
return recur(document);
function recur(doc){
let iframes = Array.from(doc.getElementsByTagName('iframe'));
for(const e of iframes){
try{
iframes.push.apply(iframes, recur(e.contentDocument));
} catch(err) {
/* Ignore: Access blocked by XSite policy. */
}
}
return iframes;
}
}
function reportError(err, msg){
msg = Array.from(msg);
msg.push('\n',err);
console.log.apply(console, msg);
window.readerModeLastError = msg;
}
//// ====== EXECUTE ======
function execTask(func){
try{
func();
} catch(err) {
reportError(err, ["FAILED TO EXECUTE TASK", func.name]);
}
}
startupTasks.map(execTask);
periodicTasks.map(execTask);
window.setInterval(
function execRepeat(){
periodicTasks.map(execTask);
},
1000);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment