/** * Make a page more readable by disabling all page styling and applying a * bare minimum of our own. Go to the first thing that looks like the start * of the actual content so no time is wasted scrolling past initial * navigation etc. * * @title Readable++ */ (function read() { /* Create a new IFRAME to get a "clean" Window object, so we can use its * console. Sometimes sites (e.g. Twitter) override console.log and even * the entire console object. "delete console.log" or "delete console" * does not always work, and messing with the prototype seemed more * brittle than this. */ var console = (function () { var iframe = document.getElementById('xxxJanConsole'); if (!iframe) { iframe = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe'); iframe.id = 'xxxJanConsole'; iframe.style.display = 'none'; (document.body || document.documentElement).appendChild(iframe); } return iframe && iframe.contentWindow && iframe.contentWindow.console || { log: function () {} }; })(); /* The style sheet for more readable content. */ var css = (function () { /*@charset "utf-8"; @namespace svg "http://www.w3.org/2000/svg"; -jancss-comment { content: "General styles -------------------------------------------"; } * { line-height: 1.5; } html { background: rgb(245, 245, 225); color: rgb(0, 0, 30); } body { max-width: 48em; margin: 0 auto; padding: 1em; font-family: "Calibri", sans-serif; font-size: 1.05rem; } p { line-height: 1.7; } :link { color: #00e; } :visited { color: #528; } :link:focus, :visited:focus, :link:hover, :visited:hover { color: #e30; } :link:active, :visited:active { color: #e00; } center, [align] { text-align: left; } b:not(.jancss-probably-structure), u, blink { font-weight: inherit; font-style: inherit; text-decoration: inherit } b.jancss-probably-structure { font-size: larger; } .jancss-probably-layout { font: inherit; } -jancss-comment { content: "Headers --------------------------------------------------"; } h1:not(.jancss-probably-layout), h2:not(.jancss-probably-layout), h3:not(.jancss-probably-layout) { font-family: "Cambria", serif; } h1, h1.jancss-probably-layout { border-bottom: 1px solid #888; font-size: 200%; font-weight: 100; } h2, h2.jancss-probably-layout * { border-bottom: 1px solid #bbb; font-size: 150%; font-weight: 100; } h3, h3.jancss-probably-layout { border-bottom: 1px dotted #bbb; font-size: 117%; font-weight: 100; } h1.jancss-probably-layout *, h2.jancss-probably-layout *, h3.jancss-probably-layout * { font-size: 1rem; } -jancss-comment { content: "Links in headers (probably permalinks) -------------------"; } h1 a[href]:not(:hover), h2 a[href]:not(:hover), h3 a[href]:not(:hover) { text-decoration: none; } h1 a[href]::after, h2 a[href]::after, h3 a[href]::after { font-size: 75%; content: " #"; } -jancss-comment { content: "Pre-formatted text and source code -----------------------"; } pre { padding: 1ex; border: 1px dotted; } code, pre, .syntaxhighlighter, .dp-highlighter { font-family: "Consolas", monospace; font-size: small; background: #ffe; } .dp-highlighter + pre[name="code"] { display: none; } -jancss-comment { content: "Forms ----------------------------------------------------"; } textarea { width: 100%; height: 32ex; } -jancss-comment { content: "Tables ---------------------------------------------------"; } table.jancss-probably-for-data th, table.jancss-probably-for-data td { vertical-align: top; text-align: left; padding: 0.5ex; } table.jancss-probably-for-data caption { font-weight: bold; border-bottom: 1px dotted; } table.jancss-probably-for-layout td { display: inline-block; } table.jancss-probably-for-data tr:nth-child(odd) td:not(.jancss-active-col) { background: #ffe; } table.jancss-probably-for-data tr:hover td:not(.code), table.jancss-probably-for-data .jancss-active-col { background: #ffc; } table.jancss-probably-for-data th, table.jancss-probably-for-data tr td:not(.code):hover { background: #ff9; } table.jancss-probably-for-data th code, table.jancss-probably-for-data td code { background: inherit; } -jancss-comment { content: "Make images use the full page width ----------------------"; } img, input[type="image"], object, embed, video, audio, iframe, canvas, :not(svg|*) > svg|* { max-width: 100%; } figure { margin: 0; } iframe { width: 100%; } iframe[class*="twitter"] { min-height: 15em; } -jancss-comment { content: "Dim images and media until :hover ------------------------"; } body:not(:hover) img, body:not(:hover) input[type="image"], body:not(:hover) object, body:not(:hover) embed, body:not(:hover) video, body:not(:hover) audio, body:not(:hover) iframe, body:not(:hover) canvas, body:not(:hover) :not(svg|*) > svg|* { opacity: 0.25; } -jancss-comment { content: "Limit icon dimensions --------------------------------"; } a[href*="facebook.com"] svg, a[href*="instagram.com"] svg, a[href*="twitter.com"] svg, a[href*=".pinterest."] svg, svg[id*="icon"], svg[class*="icon"], svg[class*="inline"], svg[data-icon], svg[role="img"], [class*="icon"] svg, [class*="Icon"] svg, img[class*="icon"][src*=".svg"], img[class*="Icon"][src*=".svg"], span > svg, button svg, [class*="button"] svg, [role*="button"] svg, [class*="controls"] svg, .svg-icon, .inline-icon, .wp-smiley, .smiley, .emoticon, :not(html).emoji { max-width: 1.4em; max-height: 1.4em; } -jancss-comment { content: "Make everything scrollable -------------------------------"; } [style*="position: fixed"], [style*="position:fixed"] { position: static !important; } -jancss-comment { content: "Make side notes and pull quotes less conspicuous ---------"; } aside:not(:hover), [data-expander-id], [id^="footnote_plugin_tooltip_text_"]:not(:hover), blockquote[class*="quote"]:not(:hover), q[class*="pull"]:not(:hover), blockquote[class*="pull"]:not(:hover), .quote-box:not(:hover), .su-pullquote:not(:hover), .pullquote:not(:hover), .pullQuote:not(:hover), .pull-quote:not(:hover) { opacity: 0.25; } -jancss-comment { content: "Decrease common forum and metadata font size -------------"; } .postprofile, .signature { font-size: smaller; border-top: 1px dotted; opacity: 0.5; } -jancss-comment { content: "Hide common social media elements ------------------------"; } iframe[src*=".facebook.com/"], iframe[src*=".twitter.com/widgets/"], iframe[src*="//plusone.google.com/_/+1/"], iframe[src*="//www.reddit.com/static/button/"], iframe[src*="//s7.addthis.com/"], iframe[src*="//www.stumbleupon.com/badge/embed/"], iframe[src*="//widgets.bufferapp.com/"] { width: 12em; height: 4ex; border: 1px dotted; } .twtr-widget.twtr-scroll { max-height: 30ex; overflow: auto; } .article__share { display: none; } #social_btns { display: none; } .taboola { display: none; } .social-media > .share { display: none; } :-moz-any( div, ul, li ):-moz-any( :-moz-any( [id*="social"], [id*="Social"] ):-moz-any( [id*="media"], [id*="Media"] ):-moz-any( [id*="share"], [id*="Share"], [id*="sharing"], [id*="Sharing"] ), :-moz-any( [id*="social"], [id*="Social"] [id*="share"], [id*="Share"], [id*="sharing"], [id*="Sharing"] ):-moz-any( [id*="toolbar"], [id*="Toolbar"], [id*="buttons"], [id*="Buttons"], ), :-moz-any( [class*="social"], [class*="Social"] ):-moz-any( [class*="media"], [class*="Media"] ):-moz-any( [class*="share"], [class*="Share"], [class*="sharing"], [class*="Sharing"] ), :-moz-any( [class*="social"], [class*="Social"] [class*="share"], [class*="Share"], [class*="sharing"], [class*="Sharing"] ):-moz-any( [class*="toolbar"], [class*="Toolbar"], [class*="buttons"], [class*="Buttons"], ) ) { display: none; } :matches( div, ul, li ):matches( :matches( [id*="social"], [id*="Social"] ):matches( [id*="media"], [id*="Media"] ):matches( [id*="share"], [id*="Share"], [id*="sharing"], [id*="Sharing"] ), :matches( [id*="social"], [id*="Social"] [id*="share"], [id*="Share"], [id*="sharing"], [id*="Sharing"] ):matches( [id*="toolbar"], [id*="Toolbar"], [id*="buttons"], [id*="Buttons"], ), :matches( [class*="social"], [class*="Social"] ):matches( [class*="media"], [class*="Media"] ):matches( [class*="share"], [class*="Share"], [class*="sharing"], [class*="Sharing"] ), :matches( [class*="social"], [class*="Social"] [class*="share"], [class*="Share"], [class*="sharing"], [class*="Sharing"] ):matches( [class*="toolbar"], [class*="Toolbar"], [class*="buttons"], [class*="Buttons"], ) ) { display: none; } -jancss-comment { content: "Hide ad elements that slipped through my ad blocker ------"; } iframe[id^="google_ads_"] { display: none; } -jancss-comment { content: "Hide empty list items ------------------------------------"; } li:empty, li.jancss-emptyish { display: none; } -jancss-comment { content: "Make common navigation elements more compact -------------"; } :-moz-any( nav, body [class*="avigat"], body [id*="avigat"], body [class*="-nav-"], body [class*="nav-"], body [class$="-nav"], body [id*="-nav-"], body [id*="nav-"], body [id$="-nav"], body [role="navigation"] ) ul { display: inline; margin: 0; padding: 0; } :-webkit-any( nav, body [class*="avigat"], body [id*="avigat"], body [class*="-nav-"], body [class*="nav-"], body [class$="-nav"], body [id*="-nav-"], body [id*="nav-"], body [id$="-nav"], body [role="navigation"] ) ul { display: inline; margin: 0; padding: 0; } :any( nav, body [class*="avigat"], body [id*="avigat"], body [class*="-nav-"], body [class*="nav-"], body [class$="-nav"], body [id*="-nav-"], body [id*="nav-"], body [id$="-nav"], body [role="navigation"] ) ul { display: inline; margin: 0; padding: 0; } :-moz-any( nav, body [class*="avigat"], body [id*="avigat"], body [class*="-nav-"], body [class*="nav-"], body [class$="-nav"], body [id*="-nav-"], body [id*="nav-"], body [id$="-nav"], body [role="navigation"] ) li { display: inline; margin: 0; padding: 0 .5em; border-right: 1px dotted; } :-webkit-any( nav, body [class*="avigat"], body [id*="avigat"], body [class*="-nav-"], body [class*="nav-"], body [class$="-nav"], body [id*="-nav-"], body [id*="nav-"], body [id$="-nav"], body [role="navigation"] ) li { display: inline; margin: 0; padding: 0 .5em; border-right: 1px dotted; } :any( nav, body [class*="avigat"], body [id*="avigat"], body [class*="-nav-"], body [class*="nav-"], body [class$="-nav"], body [id*="-nav-"], body [id*="nav-"], body [id$="-nav"], body [role="navigation"] ) li { display: inline; margin: 0; padding: 0 .5em; border-right: 1px dotted; } -jancss-comment { content: "Hide old cufón text replacement --------------------------"; } .cufon-canvas canvas { display: none; } -jancss-comment { content: "Make notes on decorrespondent.nl less conspicuous --------"; } .contentitem-sidenote:not(:hover) > :not(.contentitem-sidenote-snippet), .contentitem-infocard-toggle-container + .contentitem-infocard-contents:not(:hover) { opacity: 0.25; } .contentitem-sidenote:hover > :not(.contentitem-sidenote-snippet), .contentitem-infocard-toggle-container + .contentitem-infocard-contents:hover { background: #ffc; } -jancss-comment { content: "Hide the source text on Google Translate-d pages ---------"; } .google-src-text { display: none; } -jancss-comment { content: "Hide big ScrollMagic spacers, e.g. on Co.Design ----------"; } .scrollmagic-pin-spacer { display: none; } -jancss-comment { content: "Always hide our IFRAME used to restore console.log -------"; } #xxxJanConsole { display: none; } */; }).toString() .replace(/^function\s*\(\s*\)\s*\{\s*\/\*/, '') .replace(/\*\/\s*\;?\s*\}\s*$/, ''); /* The attributes to disable. */ var attrs = [ 'style', 'background', 'bgcolor', 'color', 'text', 'link', 'vlink', 'alink', 'hlink', 'table@width', 'colgroup@width', 'col@width', 'tr@width', 'td@width', 'th@width', 'table@height', 'tr@height', 'td@height', 'th@height', 'img@width', 'img@height', 'border', 'frameborder', 'align', 'face', 'font@size', 'basefont@size' ]; /* Elements to remove completely. */ var elementsToRemoveSelectors = [ '.bt-popin' /* Used on standaard.be. */ ]; /* The selectors to try for the header elements, whose text content will be compared to the page title. The last match wins. */ var headerSelectors = [ '[class*="head"]:not(:empty)', '[class*="Head"]:not(:empty)', '[id*="head"]:not(:empty)', '[id*="Head"]:not(:empty)', '[role="heading"]', '[class*="title"]:not(:empty)', '[class*="Title"]:not(:empty)', 'h1:not(:empty), h2:not(:empty), h3:not(:empty)', 'h1:not(:empty)[itemprop~="name"], h2:not(:empty)[itemprop~="name"], h3:not(:empty)[itemprop~="name"]', 'h1:not(:empty)[itemprop~="headline"], h2:not(:empty)[itemprop~="headline"], h3:not(:empty)[itemprop~="headline"]' ]; var ancestorsForHeadersToIgnoreSelectors = [ 'aside', '.related-posts', '.article-slider', '.article-drawer' ]; /* The selectors to try (in this order) for the first content element to scroll to when no suitable header was found. */ var contentSelectors = [ /* The most semantically rich elements should be used correctly so we * can ass-u-me them to be the main content element, right? */ 'main h1:not(:empty)', 'main header', 'main h2:not(:empty)', 'main', 'body [itemprop="blogPost"] h1:not(:empty)', 'body [itemprop="blogPost"] header', 'body [itemprop="blogPost"] h2:not(:empty)', 'body [itemprop="blogPost"]', 'body [role="main"] h1:not(:empty)', 'body [role="main"] header', 'body [role="main"] h2:not(:empty)', 'body [role="main"]', 'body [role="document"] h1:not(:empty)', 'body [role="document"] header', 'body [role="document"] h2:not(:empty)', 'body [role="document"]', 'body [role="article"] h1:not(:empty)', 'body [role="article"] header', 'body [role="article"] h2:not(:empty)', 'body [role="article"]', 'body #main h1:not(:empty)', 'body #main header', 'body #main h2:not(:empty)', 'body #main', /*
is also "semantically rich", but there are several sites * that have a list of related articles, each in its own
. A * "real" article would not have any
siblings. */ ':not(li) > article:only-of-type', /* Common IDs and classes for the main content element (e.g. weblog * post IDs, newspaper articles, …) */ 'body #article', 'body :not(#spotlight) > .article', 'body .articleContent', 'body #article_top', 'body #article_body', 'body #article_main', 'body .post-body:not(.field-item)', ':not(input):not(textarea).post', ':not(input):not(textarea).blogpost', ':not(input):not(textarea).blogPost', 'body [id^="post0"]', 'body [id^="post1"]', 'body [id^="post2"]', 'body [id^="post3"]', 'body [id^="post4"]', 'body [id^="post5"]', 'body [id^="post6"]', 'body [id^="post7"]', 'body [id^="post8"]', 'body [id^="post9"]', 'body [id^="post-0"]', 'body [id^="post-1"]', 'body [id^="post-2"]', 'body [id^="post-3"]', 'body [id^="post-4"]', 'body [id^="post-5"]', 'body [id^="post-6"]', 'body [id^="post-7"]', 'body [id^="post-8"]', 'body [id^="post-9"]', 'body #entry', 'body .entry', 'body #content', 'body .content', 'body [id^="content"]', 'body [class^="content"]', 'body #main', 'body .main', /* Consider the first header (in DOM order) to be the most important * one and ass-u-me it is the start of the main content. */ 'h1:not(:empty)', 'body #header', 'header', 'body .header', 'h2:not(:empty)', /* When all else fails, just look for bigger text, which would * probably be used instead of the appropriate header elements. */ 'big' ]; /* Structure elements incorrectly used for layout purposes ("make it big and bold"). */ var structureElementsForLayoutSelectors = [ '//*[contains(" h1 h2 h3 h4 h5 h6 h7 ", concat(" ", local-name(), " ")) and string-length(normalize-space()) > 120]' ]; /* Layout elements incorrectly used for structure purposes ("bold means header"). */ var layoutElementsForStructureSelectors = [ /* Because there is no support for the Selectors Level 4 "subject of * a selector" syntax yet (or any definite syntax, for that matter), * I simply ass-u-me in the code below that the subject is a "B" * element. Either the result of the selector, or the previous * element sibling. */ 'b:first-child + :empty', ':empty + b + :empty', ':empty + b:last-child', 'div > b:only-child, p > b:only-child' ]; /* URI pattern for syntax highlighting style sheets. */ var syntaxHighlightHrefRegex = /\b((syntax|pygments)(hi(ghlight(er)?|lite(r)?))?|sh(Core|Theme[^.]*)|geshi|codecolorer)[./]/i; /* Keep track of which elements have had their event handlers * disabled/re-enabled. */ var elementsWithToggledEventHandlers = {}; var eventHandlerAttributesToToggle = [ 'oncontextmenu', 'onshow', 'oninput', 'onkeydown', 'onkeyup', 'onkeypress', 'onmousedown', 'onmouseup', 'onmouseenter', 'onmouseleave', 'onmouseover', 'onmouseout', 'onmousemove', 'onresize', 'onscroll', 'onwheel', 'onselect', 'onselectstart', 'onselectionchange' ]; /* All main content elements (the one in the main document and those * nested in any IFRAMEs etc.) */ var contentElements = []; /* The main function. */ (function execute(document) { function addClass(element, classNames) { /* HTMLElement.classList does not work on iOS 4's Safari, so this is a fallback. */ classNames.split(/\s+/).forEach(function (className) { element.className = ((' ' + element.className + ' ').replace(' ' + className.trim() + ' ', '') + ' ' + className).trim(); }); } function removeClass(element, classNames) { classNames.split(/\s+/).forEach(function (className) { element.className = (' ' + element.className + ' ').replace(' ' + className.trim() + ' ', '').trim(); }); } function toArray(arrayLike) { return Array.prototype.slice.call(arrayLike); } var all = toArray(document.getElementsByTagName('*')), ourStyleSheet = document.getElementById('jancss'), allStyleSheets = toArray(document.styleSheets), prettyPrintStyleSheet, matches; /* Special hack for The Guardian (and possibly others), which re-enables the CSS because it detects a change in font size. */ window.TextResizeDetector && TextResizeDetector.stopDetector && TextResizeDetector.stopDetector(); /* Clear all scheduled callbacks. Naively ass-u-me that any call to * setTimeout/setInterval returns the next ID from a monotonically * increasing function that is used for both timeout and interval * IDs. Therefore, to clear all timeouts and intervals, it suffices * to get a new timeout ID and then clear everything up to that ID. * * Though the HTML5 specification says nothing about the return value * of setTimeout/setInterval, this appears to work in Firefox 22, * Chrome 27, Opera 12 and Safari 5. */ var maxTimeoutId = setTimeout(function () { for (var i = 1; i < maxTimeoutId; i++) { clearTimeout(i); clearInterval(i); } }, 4); /* 4 ms is the minimum timeout as per HTML5. */ /* Now clear all the requested animation frame callbacks. Again, this * naively assumes the ID will increment one by one. MDN explicitly * advises against assumptions such as this one: https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame#Return_value */ var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || function () { }; var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame || window.msCancelAnimationFrame || function () { }; var maxAnimationFrameRequestId = requestAnimationFrame(function () { for (i = 1; i < maxAnimationFrameRequestId * 2; i++) { cancelAnimationFrame(i); } /* Log requests to the original requestAnimationFrame. */ window.requestAnimationFrame = function (callback) { var callbackSource = callback.toSource && callback.toSource(); if (callbackSource && callbackSource.indexOf('Readable++ requestAnimationFrame interceptor') === -1) { console.log('Readable++: intercepted call to requestAnimationFrame at ' + new Date()); console.log('Readable++: callback for requestAnimationFrame: ' + callbackSource); } }; }); /* While in Readable++ mode, disable some elements' event handlers. * This prevents hijacking events like "scroll" and "resize", which * can be abused to annoy me with visual effects and whatnot, and * "contextmenu" and "selectstart", which are clearly defined as * universal and inalienable human rights. Look it up at your local * Wikipedia office. */ [window, document, document.documentElement, document.body].forEach(function (elem) { /* In non-HTML documents, elements like document.body can be * null. */ if (!elem) { return; } /* Because the window for IFRAMEs is the same as the outer * document's window, we need to keep track of wether we * have toggled the event handlers. Otherwise, they might * get disabled and re-enabled immediately after. */ if (elementsWithToggledEventHandlers[elem]) { return; } elementsWithToggledEventHandlers[elem] = true; /* Toggle selected event handlers that have been set using * "elem.oneventx = function () { … };" */ eventHandlerAttributesToToggle.forEach(function (attrib) { if (elem['jancss-' + attrib]) { elem[attrib] = elem['jancss-' + attrib]; delete elem['jancss-' + attrib]; } else if (elem[attrib]) { elem['jancss-' + attrib] = elem[attrib]; elem[attrib] = function () { }; } }); /* Toggle all event handlers that have been set using jQuery. */ if (typeof jQuery === 'function') { /* Since jQuery 1.7. */ if (typeof jQuery.hasData === 'function' && jQuery.hasData(elem)) { /* Spotted in the wild: "jQuery._data is undefined". */ if (typeof jQuery._data !== 'function') { return; } var data = jQuery._data(elem); if (data.jancssEvents) { data.events = data.jancssEvents; delete data.jancssEvents; jQuery._data(elem, data); return; } else if (data.events) { data.jancssEvents = data.events; delete data.events; jQuery._data(elem, data); return; } } /* Before jQuery 1.7 and after jQuery 1.2.3 */ if (jQuery.fn.data) { var $elem = jQuery(elem); var eventsData = $elem.data('events'); var jancssEventsData = $elem.data('jancssEvents'); if (jancssEventsData) { $elem.data('events', jancssEventsData); $elem.removeData('jancssEvents'); } else if (eventsData) { $elem.data('jancssEvents', eventsData); $elem.removeData('events'); } } } }); /* The code above does not work for event listeners added using * addEventListener. Some sites listen for the "resize" event and * then reposition elements by setting their style directly. To * counteract this, simply delete all "style" attributes that get set * while our style sheet is enabled. That'll show 'em! */ if (typeof MutationObserver === 'function' && !document.jancssHasMutationObserver) { document.jancssHasMutationObserver = true; var observer = new MutationObserver(function (mutations) { if (ourStyleSheet.disabled) { return; } mutations.forEach(function(mutation) { if (!mutation.target.hasAttribute('style') || mutation.target.id === 'xxxJanConsole' || mutation.target.xxxJanReadableAllowStyle) { return; } console.log('Readable++: removing "style" attribute set while in Readable++ mode on element ', mutation.target); mutation.target.removeAttribute(mutation.attributeName); }); }); observer.observe(document, { attributes: true, attributeFilter: ['style'], subtree: true }); } /* Load bLazy.js "retina" images. This is more specific than the * generic lazy-loading attributes below, and needs special handling * because it specifies multiple sources in one attribute. */ toArray(document.querySelectorAll('img.b-lazy[data-src*="|"]')).forEach(function (img) { var attribute = 'data-src'; var sources = img.getAttribute(attribute).split('|'); img.src = sources.pop(); img.removeAttribute(attribute); }); /* Load Riloadr "