Sleazy Fork is available in English.

nHentai QoL

Tracks history, removes annoying characters from comments, and links replies in comments

// ==UserScript==
// @name         nHentai QoL
// @namespace    http://tampermonkey.net/
// @version      2.2.0
// @description  Tracks history, removes annoying characters from comments, and links replies in comments
// @author       Exiua
// @match        https://nhentai.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=nhentai.net
// @license      MIT
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

// Blacklisted tags will be highlighted red
const blacklist = [];
// Warning tags will be highlighted orange
const warning = [];

const commentCleaningRegex = /\u3662+/gm;
const postIdRegex = /\/g\/(\d+)\/?/gm;
const pageRegex = /\/\d+\/\d+\//gm;

(function() {
    'use strict';

    window.addEventListener('load', main);
    //setTimeout(main, 10000);
})();

function main(){
    checkIfReloadNeeded();

    const currentUrl = window.location.href;
    if(currentUrl.includes("/g/")){
        commentSectionHandler();
        handlePost(currentUrl);
    }
    else{
        // Add favorites to history
        /*if(currentUrl.includes("/favorites/")){
            favLoader();
        }*/
        const mainPage = currentUrl == "https://nhentai.net/";
        handleGallery(mainPage);
    }
}

function favLoader(){
    const container = document.getElementById("favcontainer");
    for(const post of container.children){
        const id = post.getAttribute("data-id");
        const caption = post.getElementsByClassName("caption")[0];
        const title = caption.innerText;
        GM_setValue(id, title);
    }
    console.log("done");
    const nextBtn = document.getElementsByClassName("next")[0];
    nextBtn.click();
}

function handlePost(url){
    const match = postIdRegex.exec(url);
    const postId = match[1];
    const title = document.getElementsByClassName("title")[0].innerText;
    GM_setValue(postId, title);
    if(pageRegex.test(url)){
        return;
    }

    const tagContainer = document.getElementById("tags");
    for(const tagSet of tagContainer.children){
        const tags = tagSet.getElementsByClassName("tags")[0];
        checkTags(tags);
    }

    const container = document.getElementById("related-container");
    checkPosts(container);
}

function checkTags(tags){
    for(const tag of tags.children){
        const nameTag = tag.getElementsByClassName("name")[0];
        if(nameTag == null){
            continue;
        }

        const name = nameTag.innerText;
        if(blacklist.includes(name)){
            addColor(tag, "#FF0000");
        }
        else if(warning.includes(name)){
            addColor(tag, "#FF9900");
        }
    }
}

function addColor(elem, color){
    elem.setAttribute("style", "color: " + color + ";");
}

function handleGallery(mainPage){
    if(mainPage){
        const popularContainer = document.getElementsByClassName("container index-container index-popular")[0];
        checkPosts(popularContainer);
    }

    const container = document.getElementsByClassName("container index-container")[0];
    checkPosts(container);
}

function checkPosts(postsParent){
    for(const post of postsParent.children){
        if(post.tagName != "DIV"){
            continue;
        }

        const classes = post.getAttribute("class");
        if (classes.includes("blacklisted")){
            continue;
        }

        const anchor = post.getElementsByTagName("a")[0];
        const href = anchor.getAttribute("href");
        postIdRegex.lastIndex = 0;
        const match = postIdRegex.exec(href);
        if(GM_getValue(match[1]) == null){
            continue;
        }

        fadePost(post);
    }
}

function fadePost(post){
    const img = post.getElementsByTagName("img")[0];
    img.setAttribute("style", "opacity: 20%;");
}

function checkIfReloadNeeded(){
    const errors = document.getElementsByTagName("h1");
    if(errors.length == 0){
        return;
    }

    const error = errors[0];
    if(error.innerText != "429 Too Many Requests"){
        return;
    }

    const timeout = getRandomInt(500, 1000);
    setTimeout(reload, timeout);
}

function reload(){
    location.reload();
}

function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function cleanText(){
    const comments = document.getElementById("comments");
    for(const child of comments.children){
        const commentBody = child.getElementsByClassName("body")[0];
        let oldText = commentBody.innerText;
        let newText = oldText.replace(new RegExp("[\\u0300-\\u0e4e]+", "gm"), "").replace(commentCleaningRegex, "");
        commentBody.innerText = newText;
        if(newText !== oldText){
            console.log("Cleaned comment");
        }

        const username = child.getElementsByClassName("left")[0].getElementsByTagName("a")[0];
        oldText = commentBody.innerText;
        newText = oldText.replace(new RegExp("[\\u0300-\\u0e4e]+", "gm"), "").replace(commentCleaningRegex, "");
        commentBody.innerText = newText;
        if(newText !== oldText){
            console.log("Cleaned username");
        }
    }
}

function waitForElm(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                resolve(document.querySelector(selector));
                observer.disconnect();
            }
        });

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

function commentSectionHandler(){
    console.log("Waiting for comments");
    waitForElm("#comments > .comment").then((_) => {
        console.log("Found comments");
        setTimeout(() => {
            cleanText();
            linkReplies();
            anchorTagLinks();
        }, 100);
    });
}

function anchorTagLinks(){
    const comments = document.getElementById("comments");
    const children = comments.children;
    for(const comment of children){
        const body = comment.getElementsByClassName("body")[0];
        const words = body.innerHTML.split(" ");
        const links = [];
        let i = 0;
        for(const word of words){
            const [wordPart, punctuation] = getPunctuation(word);
            if (isNumeric(wordPart)){
                const link = {
                    text: wordPart,
                    href: `https://nhentai.net/g/${wordPart}/`,
                    punctuation: punctuation,
                    index: i,
                };
                links.push(link);
                //console.log(link);
            }
            else if(wordPart.startsWith("https://")){
                const link = {
                    text: wordPart,
                    href: wordPart,
                    punctuation: punctuation,
                    index: i,
                };
                links.push(link);
                //console.log(link);
            }
            i++;
        }

        const html = body.innerHTML.split(" ");
        for(const link of links){
            const anchorRaw = `<a href="${link.href}">${link.text}</a>`;
            const anchor = link.punctuation != null ? anchorRaw + link.punctuation : anchorRaw;
            //const toReplace = link.punctuation != null ? link.text + link.punctuation : link.text;
            //html.replace(toReplace, anchor);
            html[link.index] = anchor;
            console.log(html);
        }

        body.innerHTML = html.join(" ");
        //console.log(body);
    }
}

function getPunctuation(word) {
    const punctuation = ['.', ',', '!', '?', ';', ':'];
    let lastChar = word[word.length - 1];

    if (punctuation.includes(lastChar)) {
        return [word.slice(0, -1), lastChar];
    }

    return [word, null];
}

// See: https://stackoverflow.com/a/175787
function isNumeric(str) {
    if (typeof str != "string") return false // we only process strings!
    return !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
        !isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail
}

function linkReplies(){
    const comments = document.getElementById("comments");
    const children = comments.children;
    let spaces = 0;
    const commentLinks = {};
    for(let i = children.length - 1; i >= 0; i--){
        const child = children[i];
        const id = child.getAttribute("id");
        const bodyWrapper = child.getElementsByClassName("body-wrapper")[0];
        const username = bodyWrapper.getElementsByTagName("a")[0].innerText.toLowerCase().trim();
        //console.log("Username: \"" + username + "\"");
        const spaceCount = (username.match(/ /g) || []).length;
        spaces = Math.max(spaceCount, spaces);
        //console.log("Max Spaces: " + spaces);
        commentLinks[username] = id;
        const body = child.getElementsByClassName("body")[0];
        const bodyParts = body.innerText.split(" ");
        let bodyPartIndex = -1;
        for(const part of bodyParts){
            bodyPartIndex++;
            if(!part.startsWith("@")){
                continue;
            }

            let found = false;
            for(let j = 0; j <= spaces && !found; j++){
                // Remove '@'
                const extraParts = bodyParts.slice(bodyPartIndex + 1, bodyPartIndex + 1 + j).join(" ");
                const rawUsername = part.substring(1) + (extraParts === "" ? "" : " " + extraParts);
                const username = rawUsername.toLowerCase();
                //console.log("Tagged Username: \"" + username + "\"");
                for(const user in commentLinks){
                    //console.log(`Checking: ${user} === ${username}, Result: ${user === username}`);
                    if(user !== username && !username.startsWith(user)){
                        continue;
                    }

                    const id = commentLinks[user];
                    const anchor = "<a href=\"#" + id + "\">@" + rawUsername + "</a>";
                    const relpaceTarget = "@" + rawUsername;
                    //console.log("Replace Target: " + relpaceTarget + ", Replacement: " + anchor);
                    let bodyHtml = body.innerHTML;
                    bodyHtml = bodyHtml.replace(relpaceTarget, anchor);
                    //console.log(bodyHtml);
                    body.innerHTML = bodyHtml;
                    //console.log(body.innerHTML, body);
                    found = true;
                    break;
                }
            }
        }
    }
}