您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
This script modifies image posts to link directly to their high-resolution version. The link is available as soon as a box appears around an image. The color of the box indicates the image height. Secondly, the script displays the post timestamp in the upper-right corner.
// ==UserScript== // @name BDSMLR - clickable links to original high-res images and display timestamps // @namespace bdsmlr_linkify // @version 4.2.2 // @license GNU AGPLv3 // @description This script modifies image posts to link directly to their high-resolution version. The link is available as soon as a box appears around an image. The color of the box indicates the image height. Secondly, the script displays the post timestamp in the upper-right corner. // @author marp // @homepageURL https://gf.zukizuki.org/en/users/204542-marp // @match https://bdsmlr.com/ // @match https://bdsmlr.com/dashboard* // @match https://*.bdsmlr.com/ // @match https://*.bdsmlr.com/post/* // @match https://bdsmlr.com/search/* // @match https://*.bdsmlr.com/search/* // @match https://bdsmlr.com/blog/* // @match https://bdsmlr.com/originalblogposts/* // @match https://bdsmlr.com/likes* // @grant GM_xmlhttpRequest // @connect bdsmlr.com // @run-at document-start // ==/UserScript== // jshint esversion:8 //console.info("START href: ", window.location.href); //------------------------------------------------------------ // FIRST PART OF SCRIPT #2 - function that gets called by event oberver that is registered as part of 1st part #1 (see very end of this script) //------------------------------------------------------------ function createImageLinks(myDoc, myContext) { // console.info("createImageLinks: ", myContext); if (myDoc===null) myDoc = myContext; if (myDoc===null) return; if (myContext===null) myContext = myDoc; var tmpstr; var singlematch; var matches, matches2; var imageurl; var imagesrc; var imagesrcfixed; var cdnmatches; var cdnnumber; // iterate over all posts within the supplied context matches = myDoc.evaluate("./descendant-or-self::div[contains(@class,'postholder')]" + " | " + "./descendant-or-self::div[contains(@class,'post_content')]" + " | " + "./descendant-or-self::div[contains(@class,'commenttext')]" , myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var i=0, el; (i<matches.snapshotLength); i++) { el = matches.snapshotItem(i); if (el) { try { // iterate over all images // skip over items that already have a link to a "non-cdn" bdsmlr url or that are not bdsmlr links at all matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + "//*[(self::a or self::div) and (@href='') or ((contains(@class,'magnify') or contains(@class,'image-link')) and (contains(@href,'https://cdn') or contains(@href,'https://ocdn')) and contains(@href,'.bdsmlr.com'))]/img" + " | " + ".//div[contains(@class,'image_container') or contains(@class,'image_content')]//div[contains(@class,'textcontent')]" + "//img[(contains(@src,'https://cdn') or contains(@src,'https://ocdn')) and contains(@src,'.bdsmlr.com')]" + " | " + "./descendant-or-self::div[contains(@class,'singlecomment') or contains(@class,'commenttext')]" + "//img[(contains(@src,'https://cdn') or contains(@src,'https://ocdn')) and contains(@src,'.bdsmlr.com') and (contains(@class,'fr-dib') or contains(@class,'fr-fic'))]" , el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var j=0, image, imageparent; (j<matches2.snapshotLength); j++) { image=matches2.snapshotItem(j); if (image) { imagesrc = image.src; imageparent = image.parentNode; imageurl = imageparent.getAttribute("href"); if (imageurl === null || imageurl.length < 5) { imageurl = image.getAttribute("src"); // No idea why this is needed... DevTools inspector always shows a valid image src attribute... but at script execution time... apparently not... seems to be some bdsmlr JavaScript post-processing... if (imageurl === null || imageurl.length < 5) { imageurl = image.getAttribute("data-echo"); } } // CDNO08 seem to have been broken during the botched infrastructure upgrade in early 2021 // (after which the ownership of bdsmlr seems to have changed - and the new team doesn't know all the history and thus issues) // CDNO08 now redirects to CDN12 - but the image is to be found on CDN08, instead?! // Also, some URLs are HTTP - which do not seem to work (error as if image does not exist) - but HTTPS does work // 2021-11-01: Same goes from cdno12 -> cdn12 (instead of only cdno08 -> cdn08) // CAREFUL: This observation is just based on a few purly-by-chance discoveries - no idea if there are other locations besides CDN012/CDN08 // 2022-10-23: All old cdn server seem to have been decommissioned and now redirect to cdn012|cdn013|cdn101 - and there's sopme weird code that points towards ocdn012|ocdn013|ocdn101 - but that seems to do nothing // With the decommissioning, mayn old images seems to have been lost for good, now :-( // Thus, for now, I'm turning off the redirections below - except the http->https one // 2023-06-15: CDN012 seems to be partially broken - but ocdn012 works as an alternative for the broken urls. But not for all urls in general -> need to test if src url is broken instead of generic replace. imagesrcfixed = null; if (imagesrc.toLowerCase().startsWith("http://")) { imagesrcfixed = "https://" + imagesrc.substring(7); } else { imagesrcfixed = imagesrc; } if (imagesrcfixed.toLowerCase().startsWith("https://cdno08.bdsmlr.com/")) { imagesrcfixed = "https://cdn08.bdsmlr.com/" + imagesrcfixed.substring(26); } if (imagesrcfixed.toLowerCase().startsWith("https://cdno12.bdsmlr.com/")) { imagesrcfixed = "https://cdn12.bdsmlr.com/" + imagesrcfixed.substring(26); } /* if (imagesrcfixed.toLowerCase().startsWith("https://cdno012.bdsmlr.com/")) { imagesrcfixed = "https://cdn012.bdsmlr.com/" + imagesrcfixed.substring(27); } */ if (imagesrcfixed.toLowerCase().startsWith("https://cdno06.bdsmlr.com/")) { imagesrcfixed = "https://cdn101.bdsmlr.com/" + imagesrcfixed.substring(26); } if (imagesrcfixed.toLowerCase().startsWith("https://cdno010.bdsmlr.com/")) { imagesrcfixed = "https://cdn101.bdsmlr.com/" + imagesrcfixed.substring(27); } if (imagesrcfixed.toLowerCase().startsWith("https://cdno05.bdsmlr.com/")) { imagesrcfixed = "https://cdn101.bdsmlr.com/" + imagesrcfixed.substring(26); } if (imagesrcfixed !== imagesrc) { image.setAttribute("src", imagesrcfixed); imagesrc = image.src; } if (imageurl && imageurl.length > 5) { getBestImageUrlPromise(imagesrc, imageurl, image) .then( (result) => { if ( (result !== null) && (result.image !== null) && (result.url !== null) ) { var linkelem; var divelem; // 2023-06-15: deal with the partially broken SRC urls - if broken -> use the determine full link as image src url, as well checkUrlHeaderOnlyPromise(result.image.src).then( {/* image src url is working - nothing to do */} , // but if it fails, use the just determined image url, instead (srcresult) => { result.image.setAttribute("src", result.url); }); // Sometimes, images in comments are weirdly structured in hierarchies with paragraph elements -> it's better to create the link as direct parent of the image itself // same goes for the element that the colored border box can be added to - there is no appropriate div element - so we use the image itself if ( (result.image.parentNode.nodeName.toLowerCase() == "p") || ((result.image.previousSibling !== null) && (result.image.previousSibling.nodeName.toLowerCase() == "p")) || ((result.image.nextSibling !== null) && (result.image.nextSibling.nodeName.toLowerCase() == "p")) ) { linkelem = insertOrChangeLinkElement(result.image.ownerDocument, result.image, result.url); divelem = result.image; } else { linkelem = insertOrChangeLinkElement(result.image.ownerDocument, result.image.parentNode, result.url); var divmatch = result.image.ownerDocument.evaluate("./ancestor::div[(contains(@class,'hide') or contains(@class,'earlycomments')) and ancestor::div[contains(@class,'post')]]", linkelem, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); divelem = divmatch.singleNodeValue; } divelem.style = "border: 5px solid grey;"; divelem.setAttribute("title", getSizeText(result.size)); getImageDimensionsPromise(result.url, divelem, result.size) .then( (result2) => { result2.element.style = "border: 5px solid " + result2.color + ";"; result2.element.setAttribute("title", getSizeText(result2.size) + " - " + result2.width + " x " + result2.height); }); } }); } } } // multi-image posts - unhide all images (instead of having to manually click on "show x more images" matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + "/div[contains(@style,'display:none')]", el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var j=0, node; (j<matches2.snapshotLength); j++) { node=matches2.snapshotItem(j); if (node) { node.style.display = "initial"; } } // multi-image posts - hide the "show x more images" element matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + "/following-sibling::div[contains(@class,'viewAll')]", el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var j=0, node; (j<matches2.snapshotLength); j++) { node=matches2.snapshotItem(j); if (node) { node.style.display = "none"; } } } catch (e) { console.warn("error: ", e); } } } } // try to find the timestamp info and display in upper right corner function displayTimestamps(myDoc, myContext) { //console.info("displayTimestamps: ", myContext); if (myDoc===null) myDoc = myContext; if (myDoc===null) return; if (myContext===null) myContext = myDoc; var matches; var tmpstr; var singlematch; var postinfo; var timestamp; var newnode; matches = myDoc.evaluate("./descendant-or-self::div[contains(@class,'feed') and @title]", myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var i=0, el; (i<matches.snapshotLength); i++) { el = matches.snapshotItem(i); if (el) { try { timestamp = el.getAttribute("title"); if (timestamp && timestamp.length>5 && timestamp.length<70) { singlematch = myDoc.evaluate(".//div[contains(@class,'post_info')]", el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); postinfo = singlematch.singleNodeValue; if (postinfo) { newnode = myDoc.createElement("div"); newnode.setAttribute("style", "float:right; margin-right: 10px; padding-right: 10px;"); newnode.innerHTML = timestamp; postinfo.appendChild(newnode); } } } catch (e) { console.warn("error: ", e); } } } } function isOriginalImageURL(imageurl) { if (imageurl === undefined || imageurl === null) { return false; } var tmpstr = imageurl.toLowerCase(); if (tmpstr.includes("bdsmlr.com/")) { var pos = tmpstr.lastIndexOf("."); var pos2 = tmpstr.lastIndexOf("-og."); if (pos > 0 && (pos2+3)==pos) { return true; } } return false; } function getOriginalImageURL(imageurl) { if (imageurl === null) { return imageurl; } var pos = imageurl.lastIndexOf("."); var pos2 = imageurl.lastIndexOf("-og."); if (pos > 0 && (pos2+3)!=pos) { return imageurl.substring(0, pos) + "-og" + imageurl.substring(pos); } else { return imageurl; } } function insertOrChangeLinkElement(myDoc, wrapElement, linkTarget) { if (wrapElement.nodeName.toLowerCase() == "a") { wrapElement.setAttribute("href", linkTarget); wrapElement.setAttribute("target", "_blank"); return wrapElement; } else { var parentNode = wrapElement.parentNode; var newNode = myDoc.createElement("a"); newNode.setAttribute("href", linkTarget); newNode.setAttribute("target", "_blank"); parentNode.replaceChild(newNode, wrapElement); newNode.appendChild(wrapElement); return newNode; } } function getSizeText(sizeInBytes) { if (sizeInBytes === null) { return ""; } if (sizeInBytes >= 1048576) { return (sizeInBytes / 1048576).toFixed(1) + " MB"; } else if (sizeInBytes >= 1024) { return (sizeInBytes / 1024).toFixed(0) + " KB"; } else { return sizeInBytes.toFixed(0) + " B"; } } // This ASYNC method returns a promise to retrieve the HTTP response header data for the supplied URL. // It uses an "HTTP HEAD" request which does NOT download the response payload (to minimize network traffic) async function checkUrlHeaderOnlyPromise(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'HEAD', url: url, headers: { "referer": "bdsmlr.com" }, onload: function(response) { if ((response.readyState >= 2) && (response.status == 200)) { var contentLength = -1; var conlenstr = response.responseHeaders.split("\r\n").find(str => str.toLowerCase().startsWith("content-length: ")); if (conlenstr !== undefined) { contentLength = parseInt(conlenstr.slice(16), 10); if (isNaN(contentLength)) { contentLength = -1; } } resolve( { url: response.finalUrl, size: contentLength, origurl: url} ); } else { reject( { url: url, size: -1, origurl: url} ); } }, ontimeout: function(response) { reject( { url: url, size: -1, origurl: url} ); }, onerror: function(response) { reject( { url: url, size: -1, origurl: url} ); } }); }); } // This ASYNC method returns a promise to determine the url of the originally uploaded image (the one with a suffix of "-og" in the name) // This requires "testing" a lot of URLs, i.e. it causes server traffic (especially if it is an image on the "old" CDN servers from BDSMLR's early times). // To minimize network traffic, this method only requests the HTTP headers for all these URLs - the image itself nor error webpages (404) are not fully downloaded. // If multiple "-og" variants are found, the one with the largest size (in bytes, not image dimensions!) is chosen. // If the og version is smaller than the non-og version it still sticks with the og, if it is larger than 5kb (5kb to exclude error pages, the non-og is typically upscaled and worse quality than the og) // "imageelement" is only passed-through - it is a helper to supply the DOM context to the surrounding asynchronous promise "then" function of the caller // IMPORTANT DISCOVERY: For some(!) image servers, the path part (the part after hostname) is CASE-SENSITIVE! Urgh!! async function getBestImageUrlPromise(imageurl, linkurl, imageelement) { var urlsToCheckPromises = []; var uniqueset = new Set(); // Use a Set to collect all URLs that are to be tested - this automatically eliminates dups that the URL selection logic below might create var matches; var imageurl_cdnnum; var linkurl_cdnnum; var imageurl_cdnnumstr; var linkurl_cdnnumstr; var imageurl_path; var linkurl_path; var bestImageUrl; var bestImageSize; var bestImageIsOG; //console.info("getBestImageUrlPromise-Enter: ", "imageurl:" + imageurl + " linkurl:" + linkurl); // get CDN image server number as int and as string matches = imageurl.toLowerCase().match(/https?:\/\/o?cdno?(0?[1-9][0-9]*)\.bdsmlr\.com\//i); if (matches !== null && matches.length > 0) { imageurl_cdnnumstr = matches[1]; imageurl_cdnnum = parseInt(matches[1], 10); } else { imageurl_cdnnum = NaN; } // get CDNO image server number as int and as string matches = linkurl.toLowerCase().match(/https?:\/\/o?cdno?(0?[1-9][0-9]*)\.bdsmlr\.com\//i); if (matches !== null && matches.length > 0) { linkurl_cdnnumstr = matches[1]; linkurl_cdnnum = parseInt(matches[1], 10); } else { linkurl_cdnnum = NaN; } // get non-hostname part of the url, including leading "/" (NOTE: this regex fixes buggy urls with multiple "//" after the hostname) matches = imageurl.match(/https?:\/\/[^.]*\.?bdsmlr\.com\/*(\/.+$)/i); if (matches !== null && matches.length > 0) { imageurl_path = matches[1]; } else { imageurl_path = null; } // get non-hostname part of the url, including leading "/" (NOTE: this regex fixes buggy urls with multiple "//" after the hostname) matches = linkurl.match(/https?:\/\/[^.]*\.?bdsmlr\.com\/*(\/.+$)/i); if (matches !== null && matches.length > 0) { linkurl_path = matches[1]; } else { linkurl_path = null; } // fetch promise for the image that is currently shown in the webpage (ALLOW redirection on this URL) uniqueset.add(imageurl); // image to which the current unmodified link in the webpage is pointing to (ALLOW redirection on this URL) uniqueset.add(linkurl); // append "-og" suffix to the original image and link urls (without modifying the hostname) uniqueset.add(getOriginalImageURL(imageurl)); uniqueset.add(getOriginalImageURL(linkurl)); if (!isNaN(imageurl_cdnnum) && (imageurl_cdnnum <= 13 || imageurl_cdnnum == 101)) { uniqueset.add(getOriginalImageURL("https://cdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://cdno" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://ocdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path)); uniqueset.add("https://cdno" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path); uniqueset.add("https://cdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path); uniqueset.add("https://ocdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path); uniqueset.add(getOriginalImageURL("https://cdn0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://cdno0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://ocdn0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path)); uniqueset.add("https://cdn0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path); uniqueset.add("https://cdno0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path); uniqueset.add("https://ocdn0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path); uniqueset.add(getOriginalImageURL("https://cdn012.bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://cdno012.bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://ocdn012.bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://cdn013.bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://cdno013.bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://cdn101.bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://cdno101.bdsmlr.com" + imageurl_path)); } if (!isNaN(linkurl_cdnnum) && (linkurl_cdnnum <= 13 || linkurl_cdnnum == 101)) { uniqueset.add(getOriginalImageURL("https://cdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path)); uniqueset.add(getOriginalImageURL("https://cdno" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path)); uniqueset.add(getOriginalImageURL("https://ocdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path)); uniqueset.add("https://cdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path); uniqueset.add("https://cdno" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path); uniqueset.add("https://ocdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path); uniqueset.add(getOriginalImageURL("https://cdn0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path)); uniqueset.add(getOriginalImageURL("https://cdno0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path)); uniqueset.add(getOriginalImageURL("https://ocdn0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path)); uniqueset.add("https://cdn0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path); uniqueset.add("https://cdno0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path); uniqueset.add("https://ocdn0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path); uniqueset.add(getOriginalImageURL("https://cdn012.bdsmlr.com" + linkurl_path)); uniqueset.add(getOriginalImageURL("https://cdno012.bdsmlr.com" + linkurl_path)); uniqueset.add(getOriginalImageURL("https://ocdn012.bdsmlr.com" + imageurl_path)); uniqueset.add(getOriginalImageURL("https://cdn013.bdsmlr.com" + linkurl_path)); uniqueset.add(getOriginalImageURL("https://cdno013.bdsmlr.com" + linkurl_path)); uniqueset.add(getOriginalImageURL("https://cdn101.bdsmlr.com" + linkurl_path)); uniqueset.add(getOriginalImageURL("https://cdno101.bdsmlr.com" + linkurl_path)); } for (var str of uniqueset) { urlsToCheckPromises.push(checkUrlHeaderOnlyPromise(str)); } bestImageUrl = null; bestImageSize = -1; bestImageIsOG = false; await Promise.allSettled(urlsToCheckPromises). then( (results) => results.forEach((result) => { // If this result is a better image than currently known - replace and use this one as next best known image if (result.status == "fulfilled") { if ( (isOriginalImageURL(result.value.url) && !bestImageIsOG && (result.value.size > 5120)) || ( (result.value.size > bestImageSize) && (result.value.size > 5120) && (isOriginalImageURL(result.value.url) == bestImageIsOG) ) ) { bestImageSize = result.value.size; bestImageUrl = result.value.url; bestImageIsOG = isOriginalImageURL(bestImageUrl); } } })); return {url: bestImageUrl, size: bestImageSize, isOG: bestImageIsOG, image: imageelement}; } // This ASYNC method gets the natural dimensions of the supplied image // This means the image needs to be downloaded fully, unfortunately! // Thus, a delay is to be expected, except if the image is already cached // Depending on the image height, the method suggests a "markup color" and then discards the downloaded image again (but does not invalidate cache). // "imageelement" is only passed-through - it is a helper to supply the DOM context to the surrounding asynchronous promise then function of the caller async function getImageDimensionsPromise(imageurl, divelement, imagesize) { var image; var imageH; var imageW; var color; // sanity check - skip full download of image if it is larger than 20MB if ( (imagesize !== null) && (imagesize > 20971520) ) { return {url: imageurl, element: divelement, width: "unknown", height: "unknown", color: "Grey", size:imagesize}; } image = new Image(); image.src = imageurl; await image.decode().then(function() { imageH = image.naturalHeight; imageW = image.naturalWidth; }); image.src = "data:,"; // clear the image now that we no longer need it if (imageH >= 2160) { color = "hsl(160, 100%, 70%)"; } else if (imageH >= 1080) { color = "hsl(" + (120.0 + 40.0 * ((imageH - 1080.0) / 1080.0)) + ", 100%, " + (50.0 + 20.0 * ((imageH - 1080.0) / 1080.0)) + "%)"; // color = "Lime"; // #00FF00, HSL(120°, 100%, 50%) } else if (imageH >=270 ) { color = "hsl(" + (120.0 * ((imageH - 270.0) / 810.0)) + ", 100%, 50%)"; // color = "Red"; // #FF0000, HSL(0°, 100%, 50%) } else if (imageH < 270 && imageH > 0 ) { color = "hsl(0, 100%, " + (50.0 * (imageH / 270.0)) + "%)"; // color = "Red"; // #FF0000, HSL(0°, 100%, 50%) } else { color = "Grey"; } return {url: imageurl, element: divelement, width: imageW, height: imageH, color: color, size:imagesize}; } //------------------------------------------------------------ // FIRST PART OF SCRIPT #1 - initial statement and registration of event observer //------------------------------------------------------------ // script runs NOT in the context of an image - i.e. dashboard, blog stream, individual post // -> register event observer for future to-be-loaded posts (endless scrolling) and execute first part of script (createImageLinks & displayTimestamps) on already loaded posts // undo badly coded attempts from bdsmlr to hide unwanted posts mentioning reblog.me // -> their code breaks infinite scrolling and incorrectly hides legitimate posts // this requires that this script is set to: @run-at document-start var scriptnode = document.createElement("script"); scriptnode.setAttribute("type", "text/javascript"); scriptnode.textContent = "let byespam = function () {} "; document.head.insertBefore(scriptnode, document.head.firstChild); //console.info("scriptNodes INIT"); // with @run-at document-start, only the HEAD section will have been loaded // -> register an observer to act as latter body nodes are being loaded // -> namely, we wait for the embedded <script> node that handles endless scrolling... // -> ...to modify it as to trigger much sooner so that the scrolling experience is smoother (trigger and complete loading BEFORE reaching the end of page) var scriptobserver = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(addedNode) { if (addedNode.nodeName == "SCRIPT" && addedNode.textContent.includes("$(document).height() - $(window).height())")) { addedNode.textContent = addedNode.textContent.replaceAll("$(document).height() - $(window).height())", "$(document).height() - (5 * $(window).height()))"); scriptobserver.disconnect(); } }); }); }); var scriptconfig = { attributes: false, childList: true, characterData: false, subtree: true }; scriptobserver.observe(document, scriptconfig); // remainder of script requires @run-at document-end document.addEventListener ("DOMContentLoaded", runAtDocumentEnd); function runAtDocumentEnd() { // create an observer instance and iterate through each individual new node var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(addedNode) { createImageLinks(mutation.target.ownerDocument, addedNode); displayTimestamps(mutation.target.ownerDocument, addedNode); }); }); }); // configuration of the observer // "theme1" is the class used by the feed root node for individual user's blog (xxxx.bdsmlr.com) -> seems unstable/temporary name -> might be changed by bdsmlr var config = { attributes: false, childList: true, characterData: false, subtree: true }; // pass in the target node (<div> element contains all stream posts), as well as the observer options var postsmatch = document.evaluate(".//div[contains(@class,'newsfeed')] | .//div[contains(@class,'theme1')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var postsnode = postsmatch.singleNodeValue; //process already loaded nodes (the initial posts before scrolling down for the first time) createImageLinks(document, postsnode); displayTimestamps(document, postsnode); //start the observer for new nodes observer.observe(postsnode, config); // also observe the right sidebar blog stream on the dashboard // pass in the target node, as well as the observer options (subtree has to be true here - target nodes are further down the hierarchy) var config2 = { attributes: false, childList: true, characterData: false, subtree: true }; var sidepostsmatch = document.evaluate(".//div[@id='rightposts']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var sidepostsnode = sidepostsmatch.singleNodeValue; // sidebar does only exist on dashboard if (sidepostsnode) { //start the observer for overlays observer.observe(sidepostsnode, config2); } }