// ==UserScript==
// @name BDSMLR - clickable links to original high-res images and display timestamps
// @namespace bdsmlr_linkify
// @version 3.2
// @license GNU AGPLv3
// @description This script modifies images to link to their original ("-og") high-res version. The link is available as soon as a border appears around an image. The color of the box indicates the range of the image height. In addition, the script unhides/expands all images of large multi-image posts and displays the timestamp of the post in the upper right corner (dashboard only).
// @author marp
// @homepageURL https://greasyfork.org/en/users/204542-marp
// @include https://bdsmlr.com/
// @include https://bdsmlr.com/?group*
// @include https://bdsmlr.com/dashboard*
// @include https://bdsmlr.com/?latest*
// @include https://*.bdsmlr.com/
// @include https://*.bdsmlr.com/post/*
// @include https://bdsmlr.com/search/*
// @include https://*.bdsmlr.com/search/*
// @include https://bdsmlr.com/blog/*
// @include https://bdsmlr.com/originalblogposts/*
// @include https://bdsmlr.com/likes*
// @include https://bdsmlr.com//*
// @run-at document-end
// ==/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 origpostlink;
var origbloglink;
var origblog;
var matches, matches2;
var imageurl;
var imagesrc;
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 {
// try to find info about original poster (if this is a reblog) as well as the link to the individual (potentially reblogged) post
// both info only seem to be present on dashboard and on rightside overlay blogs - but not always on individual blogs (xxx.bdsmlr.com) or on individual blog post URLs :-(
// This info is needed because in VERY early BDSMLR days the full resolution "-og" image sometimes only existed on the original blog hostname...
// ...and these very old posts are still around...
origblog = null;
singlematch = myDoc.evaluate(".//div[contains(@class,'originalposter')]/a[contains(@href,'.bdsmlr.com/post/')]" +
" | " +
".//div[contains(@class,'original')]/a[contains(@href,'.bdsmlr.com')]",
el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
origpostlink = singlematch.singleNodeValue; // xxxx.bdsmlr.com/post/yyyyyyyy
if (origpostlink) {
tmpstr = origpostlink.getAttribute("href"); //everything after and including "/post" gets truncated away later anyway
if ( tmpstr && (tmpstr.length > 10) &&
!(tmpstr.includes("//.bdsmlr.com") ) ) { // some urls are invalid and need to be ignored ("https://.bdsmlr.com/...")
origblog = tmpstr;
}
}
if (origblog === null) {
//second method might find the originial blog URL (xxxx.bdsmlr.com) - however, often this is just a re-poster - not the original
singlematch = myDoc.evaluate(".//div[contains(@class,'post_info')]//i[contains(@class,'retweet') or contains(@class,'rbthis')]" +
"/following-sibling::a[contains(@class,'adata') or contains(@class,'ndata')]",
el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
origbloglink = singlematch.singleNodeValue; // xxxx.bdsmlr.com
if (origbloglink) {
tmpstr = origbloglink.getAttribute("href");
if ( tmpstr && (tmpstr.length > 10) &&
!(tmpstr.includes("//.bdsmlr.com") ) ) { // some urls are invalid and need to be ignored ("https://.bdsmlr.com/...")
origblog = tmpstr;
}
}
if (origblog === null) {
// if neither of the two above find anything then this is likely NOT a reblogged post but the original post -> get the orginial blog post URL
singlematch = myDoc.evaluate(".//a[(contains(@class,'adata') or contains(@class,'ndata')) and contains(@href,'.bdsmlr.com/post/')]",
el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
origpostlink = singlematch.singleNodeValue; // xxxx.bdsmlr.com
if (origpostlink) {
tmpstr = origpostlink.getAttribute("href");
if ( tmpstr && (tmpstr.length > 10) &&
!(tmpstr.includes("//.bdsmlr.com") ) ) { // some urls are invalid and need to be ignored ("https://.bdsmlr.com/...")
origblog = tmpstr;
}
}
if (origblog === null) {
if ( !window.location.href.startsWith("https://bdsmlr.com") ) {
// if no link to neither original blog nor original blog post was found then we assume that this is the original blog post or blog itself (this is a rather shaky assumtion - fingers crossed...)
origblog = window.location.href;
}
}
}
} // if none of the above worked then we're out of luck and origblog remains null
// iterate over all links to images (i.e. does NOT (yet) create links to images where none exist in the first place)
// 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')]" +
"//a[(@href='') or ((contains(@class,'magnify') or contains(@class,'image-link')) and contains(@href,'https://cdn') and contains(@href,'.bdsmlr.com'))]/img" +
" | " +
".//div[contains(@class,'image_container') or contains(@class,'image_content')]" +
"//div[(@href='') or ((contains(@class,'magnify') or contains(@class,'image-link')) and contains(@href,'https://cdn') 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') and contains(@src,'.bdsmlr.com')]" +
" | " +
"./descendant-or-self::div[contains(@class,'singlecomment') or contains(@class,'commenttext')]" +
"//img[contains(@src,'https://cdn') 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");
}
}
if (imageurl && imageurl.length > 5) {
getBestImageUrlPromise(imagesrc, imageurl, origblog, image)
.then( (result) => {
if ( (result !== null) && (result.image !== null) && (result.url !== null) ) {
var linkelem;
var divelem;
// 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 getOriginalPosterImageURL(imageurl, originalposter) {
if (originalposter === null) {
return imageurl;
}
var pos = imageurl.toLowerCase().indexOf(".bdsmlr.com");
var pos2 = originalposter.toLowerCase().indexOf(".bdsmlr.com");
if (pos > 0 && pos2 > 0) {
return originalposter.substring(0, pos2) + imageurl.substring(pos);
} else {
return imageurl;
}
}
function isOriginalImageURL(imageurl) {
if (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, allowredirect) {
return fetch(url, (allowredirect ? { redirect: 'follow', method: 'HEAD' } : { redirect: 'error', method: 'HEAD' } ) ).then(
function(response) {
if (response.ok) {
var contentLength = parseInt(response.headers.get('Content-Length'), 10);
if (isNaN(contentLength))
contentLength = 0;
if (response.redirected && allowredirect) {
return { url: response.url, size: contentLength }
} else {
return { url: url, size: contentLength };
}
} else {
return { url: url, size: -1 };
}
},
function(rejectreason) {
return { url: url, size: -1 };
});
}
// This ASYNC method returns a promise to determine the url of the originally uploaded image (the one with a suffix of "-og" in thre 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 fully downloaded.
// If multiple "-og" variants are found, the onre 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 (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
async function getBestImageUrlPromise(imageurl, linkurl, blogurl, imageelement) {
var knownImagePromise;
var urlsToCheckPromises = [];
var matches;
var imageurl_cdnnum;
var linkurl_cdnnum;
var imageurl_cdnnumstr;
var linkurl_cdnnumstr;
var imageurl_path;
var linkurl_path;
var blogurl_base;
var knownImageResult;
var bestImageUrl;
var bestImageSize;
var bestImageIsOG;
//console.info("checkAllUrlheaders-Enter: ", "imageurl:" + imageurl + " linkurl:" + linkurl + " blogurl:" + blogurl);
// get CDN image server number as int and as string
matches = imageurl.toLowerCase().match("https?:\/\/cdno?(0?[1-9][0-9]*)\.bdsmlr\.com\/");
if (matches !== null) {
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?:\/\/cdno?(0?[1-9][0-9]*)\.bdsmlr\.com\/");
if (matches !== null) {
linkurl_cdnnumstr = matches[1];
linkurl_cdnnum = parseInt(matches[1], 10);
} else {
linkurl_cdnnum = NaN;
}
// get non-hostname part of the url, including leading "/" (NOTE: fixes buggy urls with multiple "//" after the hostname)
matches = imageurl.toLowerCase().match("https?:\/\/[^.]*\.?bdsmlr\.com\/*(\/.+$)");
if (matches !== null) {
imageurl_path = matches[1];
} else {
imageurl_path = null;
}
// get non-hostname part of the url, including leading "/" (NOTE: fixes buggy urls with multiple "//" after the hostname)
matches = linkurl.toLowerCase().match("https?:\/\/[^.]*\.?bdsmlr\.com\/*(\/.+$)");
if (matches !== null) {
linkurl_path = matches[1];
} else {
linkurl_path = null;
}
// get base hostname for the originating blog
if (blogurl !== null) {
matches = blogurl.toLowerCase().match("https?:\/\/([^./]+\.bdsmlr\.com)");
if (matches !== null) {
blogurl_base = matches[1];
} else {
blogurl_base = null;
}
} else {
blogurl_base = null;
}
// fetch promise for the image that is currently shown in the webpage (ALLOW redirection on this URL)
knownImagePromise = checkUrlHeaderOnlyPromise(imageurl, true);
// image to which the current unmodified link in the webpage is pointing to (ALLOW redirection on this URL)
urlsToCheckPromises.push(checkUrlHeaderOnlyPromise(linkurl, true));
// Use a Set to collect all other URLs that are to be tested - this automatically eliminates dups that the URL selection logic below might create
var uniqueset = new Set();
// 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 <= 5) {
//old CDN servers (cdn02 - cdn05, cdno02 - cdno05 or non-cdn location)
// -> "Wild West" as to where the "-og" image variant might be "hiding"
uniqueset.add(getOriginalImageURL("https://bdsmlr.com" + imageurl_path));
if (blogurl_base !== null) {
uniqueset.add(getOriginalImageURL("https://" + blogurl_base + imageurl_path));
}
uniqueset.add(getOriginalImageURL("https://cdn02.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdn03.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdn04.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdn05.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdno02.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdno03.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdno04.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdno05.bdsmlr.com" + imageurl_path));
}
if (isNaN(linkurl_cdnnum) || linkurl_cdnnum <= 5) {
//old CDN servers (cdn02 - cdn05, cdno02 - cdno05 or non-cdn location)
// -> "Wild West" as to where the "-og" image variant might be "hiding"
uniqueset.add(getOriginalImageURL("https://bdsmlr.com" + linkurl_path));
if (blogurl_base !== null) {
uniqueset.add(getOriginalImageURL("https://" + blogurl_base + linkurl_path));
}
uniqueset.add(getOriginalImageURL("https://cdn02.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdn03.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdn04.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdn05.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdno02.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdno03.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdno04.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdno05.bdsmlr.com" + linkurl_path));
}
if (!isNaN(imageurl_cdnnum) && imageurl_cdnnum > 5) {
//new CDN servers (cdn06+)
uniqueset.add(getOriginalImageURL("https://cdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdno" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
}
if (!isNaN(linkurl_cdnnum) && linkurl_cdnnum > 5) {
//new CDN servers (cdn06+)
uniqueset.add(getOriginalImageURL("https://cdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdno" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
}
for (var str of uniqueset) {
// some BDSMLR image servers have faulty redirections that are circular - causing huge delay and not resulting in anything useful
// Thus, all URLs in the Set are to be tested WITHOUT allowing redirection
urlsToCheckPromises.push(checkUrlHeaderOnlyPromise(str, false));
}
// Get the data for the image currently shown in the webpage as starting point
knownImageResult = await knownImagePromise;
bestImageUrl = knownImageResult.url;
bestImageSize = knownImageResult.size;
bestImageIsOG = isOriginalImageURL(bestImageUrl);
// wait until all URLs have resolved (i.e. have their HTTP headers loaded with image or error info)
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 ( (isOriginalImageURL(result.value.url) && !bestImageIsOG && (result.value.size > 0)) ||
( (result.value.size > bestImageSize) && (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 get 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,m the method suggests a "markup color" and then discards the downloaded image again.
// "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 = "SpringGreen";
} else if (imageH >= 1080) {
color = "Green";
} else if (imageH >= 810) {
color = "YellowGreen";
} else if (imageH >= 540) {
color = "Yellow";
} else if (imageH >=270 ) {
color = "Orange";
} else if (imageH < 270 && imageH > 0 ) {
color = "Red";
} 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
// fix buggy redirection from BDSMLR
if ( window.location.href.includes('bdsmlr.com//') ) {
var tmpstr = window.location.href;
var pos = tmpstr.indexOf('bdsmlr.com//');
// remove the double //
window.location.assign( tmpstr.substring(0, pos+11) + tmpstr.substring(pos+12) );
}
// prevent running for URL to image or media
else if ( !(window.location.href.includes('bdsmlr.com/uploads/')) ) {
// 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);
}
}