/b/ackwash revolutions

Add tooltips to 4chan quotes (>>).

// ==UserScript==
// @name          /b/ackwash revolutions
// @namespace     http://www.4chan.org
// @description   Add tooltips to 4chan quotes (>>).
// @author        tkirby, aeosynth, VIPPER
// @include       *.4chan.org/*
// @include       *suptg.thisisnotatrueending.com/archive/*
// @exclude       *dis.4chan.org/*
// @version       0.21
// @grant         GM_xmlhttpRequest
// ==/UserScript==

const TIP_X_OFFSET = 40;        // in pixels
const BACKLINKS = true;         // 4chan imageboards only
const ISOP = ">>POST (OP)";     // style for links to OP
const ISOUT = ">>>POST";        // style for links to other threads
const ISBACK = ">>POST";        // style for backlinks
const SEPARATOR = " ";          // backlink separator (a single space)

function bw_wash(post) {
    qts = d.evaluate(".//a[contains(@class, 'quotelink')][starts-with(text(), '>>')]", post, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    backlinkid = "";
    
    for(var i = 0, qt = null; qt = qts.snapshotItem(i++); ) {
        id = qt.toString().split("#")[1];
        
        if(/>>\/(rs|\w+\/([a-zA-Z]+|$))/i.test(qt.textContent) || /dis.4chan.org/i.test(qt.href)) { // >>/rs || >>/board/text || >>/board/ || /textboard/
            continue;
        } else if(id == "p" + op) {
            qt.textContent = ISOP.replace("POST", id.replace("p", ""));
        } else if(!d.getElementById(id)) {
            getPost(qt.toString());
            out = qt.textContent.match(/\/.+\//) || "";
            qt.textContent = ISOUT.replace("POST", id.replace("p", out));
        }
        
        if(BACKLINKS) backlink(qt, id);
        qt.addEventListener("mouseover", function(e) { bw_show(e, this) }, false);
        qt.addEventListener("mousemove", function(e) { bw_track(e) }, true);
        qt.addEventListener("mouseout", function() { bw_hide() }, false);
        qt.addEventListener("mousedown", function() { bw_hide() }, false);
    }

}

function bw_hide() {
    bw_tipcell.innerHTML = "";
    bw_tooltip.style.setProperty("display", "none", "important");
}

function bw_show(e, me) {
    id = me.hash.split("#")[1];
    posthtml = "";
    
    try {
        posthtml = d.getElementById(id).innerHTML;
    } catch(err) {
        posthtml = checkCache(id, me);
    }
    // Code cleaning: input, id, [Reply]
    posthtml = posthtml.replace(/<input.*?>/ig, "");
    posthtml = posthtml.replace(/.id="[^"]+"/ig, "");
    bw_tipcell.innerHTML = posthtml;
    
    reply = d.evaluate(".//a[text()='Reply']", bw_tipcell, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    if(reply && reply.parentNode) reply.parentNode.parentNode.removeChild(reply.parentNode);
    
    // Marks the backwashed link
    backpost = d.evaluate(".//a[contains(@href, '" + (e.target.parentNode.parentNode.id) + "')]", bw_tipcell, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    if(backpost) backpost.style.setProperty("text-decoration-style", "double", "important");
    
    // Fix OP
    if(/^<div.*\"file\".*<div.*\"postInfo desktop\"/.test(posthtml)) {
        post = d.evaluate(".//div[contains(@class, 'postInfo ')]", bw_tipcell, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
        file = d.evaluate(".//div[contains(@class, 'file')]", bw_tipcell, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
        file.parentNode.insertBefore(post, file);
    }
    
    bw_track(e);
}

function bw_track(e) {
    bw_tooltip.style.setProperty("display", "", "important");
    tip_height = bw_tooltip.clientHeight / 2;
    vp_height = Math.min(d.documentElement.clientHeight, d.body.clientHeight);
    vp_width = Math.min(d.documentElement.clientWidth, d.body.clientWidth);
    vp_bottom = window.scrollY + vp_height;
    
    if(e.pageY - tip_height < window.scrollY || bw_tooltip.clientHeight > vp_height)
        tip_y_offset = window.scrollY;
    else if(e.pageY + tip_height >= vp_bottom)
        tip_y_offset = vp_bottom - bw_tooltip.clientHeight;
    else
        tip_y_offset = e.pageY - tip_height;
    
    bw_tooltip.style.top = tip_y_offset + "px";
    if(e.pageX + TIP_X_OFFSET > vp_width * 0.6) {
        bw_tooltip.style.right = vp_width + TIP_X_OFFSET - e.pageX + "px";
        bw_tooltip.style.left = "";
    } else {
        bw_tooltip.style.left = e.pageX + TIP_X_OFFSET + "px";
        bw_tooltip.style.right = "";
    }
}

function backlink(post, id) { // CLUSTERFUCK
    if(!d.getElementById(id)) return;
    var nid = d.evaluate(".//div[contains(@class, 'postInfo')][last()]", d.getElementById(id), null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    var pid = d.evaluate("ancestor::div[@id][1]/@id", post, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.textContent;
    if(new RegExp("#" + pid + "\"").test(nid.innerHTML) || !/[0-9]/.test(pid)) return;
    
    link = d.createElement("A");
    link.setAttribute("href", "#" + pid); // (/\/res\//.test(window.location) ? "" : "res/") + post.toString().match(/[0-9]+#(?=p[0-9]+)/) + pid
    link.setAttribute("onclick", "replyhl('" + pid.replace("p", "") + "')");
    link.setAttribute("style", "font-size: smaller");
    link.textContent = ISBACK.replace("POST", pid.replace("p", ""));
    link.addEventListener("mouseover", function(e) { bw_show(e, this) }, false);
    link.addEventListener("mousemove", function(e) { bw_track(e) }, true);
    link.addEventListener("mouseout", function() { bw_hide() }, false);
    link.addEventListener("mousedown", function() { bw_hide() }, false);
    
    nid.appendChild(d.createTextNode(SEPARATOR));
    nid.appendChild(link);
}

function checkCache(id, url) {
    if(!id) id = "p" + url.pathname.match(/[0-9]+_?[0-9]+/);
    for(var i in cache) {
        if(url.href.split("#")[0] == cache[i].url) {
            var responseText = cache[i].post;
            if(responseText == "Loading post")
                return ("<h3>Loading post #" + id + "...</h3>");
            var expr = new RegExp("<div class=\"postInfoM mobile\" id=\"" + id.replace("p", "pim") + ".*?<\/blockquote>");
            responseText = responseText.match(expr);
            responseText = responseText ? responseText[0] : "<h3>Error: Post not found</h3>";
            return responseText;
        }
    }
    return "<h3>Error: Post not found</h3>"; // Should never happen
}

function getPost(id) {
    cached = false;
    link = id.split("#")[0];
    for(var i in cache)
        if(link == cache[i].url) { // force cached entry to update on new cross-links
            cached = true;
            cache[i].post = "Loading post";
        }
    
    if(!cached)
        cache.push({ url : link, post : "Loading post" });
    
    GM_xmlhttpRequest({
        method : "GET",
        url : link,
        headers: { "Accept" : "application/xml" },
        onload : function(response) {
            for(var i in cache)
                if(id.split("#")[0] == cache[i].url)
                    cache[i].post = response.responseText;
        }
    });
}

const d = document;
var op = window.location.pathname.match(/thread\/([0-9]+)/) || window.location.search.match(/[0-9]+(?:$|#)/) || "";
if(op[1]) op = op[1];
var cache = [];

var bw_tipcell = d.createElement("DIV");
bw_tipcell.setAttribute("id", "bw_tipcell");
bw_tipcell.setAttribute("class", "post reply preview");
bw_tipcell.setAttribute("style", "padding: 5px; margin: 0px; border-width: 1px !important; opacity: inherit !important");

var bw_tooltip = d.createElement("DIV");
bw_tooltip.setAttribute("id", "bw_tooltip");
bw_tooltip.setAttribute("class", "postContainer");
bw_tooltip.setAttribute("style", "display: none; position: absolute; margin: 0px 1px; padding: 0px");
bw_tooltip.appendChild(bw_tipcell);
d.body.appendChild(bw_tooltip);

var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
        for(var i = 0, newpost = null; newpost = mutation.addedNodes[i++]; )
            if(newpost.nodeName.toLowerCase() == "div") bw_wash(newpost);
    });
});

observer.observe(d.body, { childList : true, subtree : true });

window.onload = function() {
    var posts = d.evaluate(".//blockquote[contains(@class, 'postMessage')]", d.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for(var i = 0, post = null; post = posts.snapshotItem(i++); )
        bw_wash(post);
}