snahp.it - Add Decode Button

Adds a 'decode' button to code elements. Can be clicked repeatedly.

// ==UserScript==
// @name         snahp.it - Add Decode Button
// @namespace    forum.snahp.it
// @version      0.5.4
// @description  Adds a 'decode' button to code elements. Can be clicked repeatedly.
// @author       Genome
// @license      MIT
// @homepage     https://fora.snahp.eu/viewtopic.php?p=1161800
// @match        https://fora.snahp.eu/viewtopic.php*
// @match        https://forum.snahp.it/viewtopic.php*
// @icon         https://fora.snahp.eu/favicon.ico
// @run-at       document-idle
// @grant        none
// ==/UserScript==

/*jshint esversion: 6 */

(function() {
    'use strict';

    const isChromium = navigator.userAgent.toLowerCase().includes('chrome');
    const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');

    let base64Rx = /([a-z0-9+\/]{20,}[=]*)/i;

    let undoBuffer = []; // 0.5.0: Add undo buffer to let you roll back your decodes in reverse order.

    // Encodes or decodes any (likely) base64 strings line by line surgically.
    function base64CodeByLine(targetText, decode = true) {
        let textLines = targetText.split("\n");
        let completeLines = [];
        for(let textLine of textLines) {
            let hasText = textLine.trim().length > 0;
            if(!hasText) {
                completeLines.push(textLine);
                continue;
            }
            if(decode && base64Rx.test(textLine)) {
                let findResult = base64Rx.exec(textLine);
                let b64Text = findResult[1];
                let targetIx = findResult.index;
                let completeLine = textLine.substring(0, targetIx) + atob(b64Text) + textLine.substring(targetIx + b64Text.length);
                completeLines.push(completeLine);
            } else {
                completeLines.push(textLine);
            }
        }
        return completeLines.join("\n");
    }

    const oldLinkRx = /links\.snahp\.it/gi;

    // Encodes or Decodes the innerText of a passed in DOM element.
    function base64CodeContents(el, decode = true) {
        debugger;
        if(el && el.innerText.trim() !== "") {
            undoBuffer.push({ "element": el, "previousText": el.innerText });
            el.innerText = base64CodeByLine(el.innerText, decode);
            // 0.5.1: Re-fix links going to links.snahp.it to the new domain.
            if(oldLinkRx.test(el.innerText)) {
                el.innerText = el.innerText.replaceAll(oldLinkRx, "lnk.snahp.eu");
            }
        }
    }

    function undoLast() {
        if(undoBuffer.length > 0) {
            let lastOp = undoBuffer.pop();
            lastOp.element.innerText = lastOp.previousText;
        }
    }

    // Copies the innerText of a given html element.
    function copyContents(el) {
        navigator.clipboard.writeText(el.innerText.trim());
    }

    // Finds hideboxes with no internal <codebox> element (aka they are bare text).
    function findBareUnhideBoxes() {
        let finalElems = []
        for (const el of document.querySelectorAll("dl.hidebox.unhide")) {
            if(el.querySelector('div.codebox') == null) {
                finalElems.push(el);
            }
        }
        return finalElems;
    }

    // Create an <a> element and quickly assign some properties (text, any classes, hardcoded style, and a 'click' action)
    function createButton(buttonText, classList, hardStyle, onClickCallback) {
        let newButton = document.createElement('a');
        newButton.innerText = buttonText;
        if(classList) classList.forEach(cl => newButton.classList.add(cl))
        if(hardStyle) newButton.style = hardStyle;
        if(onClickCallback) newButton.addEventListener("click", onClickCallback);
        return newButton;
    }

    const bareBoxButtonStyle = "margin: 0 8px 0 8px; font-weight: bold;";
    const bareBoxWrapperStyle = "float: right; text-align: right; border: 1px inset white;";

    // Adds a controls div with buttons to hideboxes WITH NO Codebox inside. Aka BareBox. (so they are bare text)
    function addBareBoxButtons() {
        // Find 'dl.hidebox.unhide' boxes with no internal codebox.
        let boxElems = findBareUnhideBoxes();
        for (let boxElem of boxElems) {
            // Add <br/> tag inside to hidebox to buffer the contents from selecting anything after.
            // v0.5.4: Only for Chromium based browsers, ignore Firefox (no selection issue)
            if(!isFirefox && isChromium) {
              let newBr = document.createElement('br');
              newBr.style.content = "\"\""; // Style fix 0.5.3 Workaround for allowing <br/> to still split text, but not show.
              boxElem.appendChild(newBr);
            }

            // Create a decode and copy button and make their click actions refer to this specific box.
            let decode = createButton("Decode", ["pointer", "noselect"], bareBoxButtonStyle, () => base64CodeContents(boxElem));
            // 0.5.0 Add ability to right click to re-encode just in case it's needed.
            decode.addEventListener("contextmenu", (ev) => {
                ev.preventDefault(); // Don't pop up the right click menu but ONLY on this button.
                ev.stopPropagation();
                undoLast();
            });
            let copy = createButton("Copy", ["pointer", "noselect"], bareBoxButtonStyle, () => copyContents(boxElem));

            // Create a wrapper div to hold both and position it next to the BareBox.
            let newControlsWrapper = document.createElement('div');
            newControlsWrapper.append(decode, copy);
            newControlsWrapper.style = bareBoxWrapperStyle;
            boxElem.insertAdjacentElement("afterend", newControlsWrapper);
        }
    }

    // Creates 'Decode' button for Codebox elements.
    function addCodeboxButtons() {
        // Find all the 'copy' buttons attached to code-boxes on the page.
        let codeboxCopyButtons = document.querySelectorAll(".codebox p a:last-child");

        // For each of the code boxes.
        for(let copyButtonEl of codeboxCopyButtons) {
            // Find the related codebox (it's a cousin element).
            let codeboxElement = copyButtonEl.parentNode.parentNode.querySelector('pre code');

            // Create a new button and bind click event to decode.
            let newButton = createButton(
                "Decode", ["pointer", "noselect"], "float: right; margin-right: 8px; margin-left: 8px;",
                () => base64CodeContents(codeboxElement)
            );
            // 0.5.0 Add ability to right click to re-encode just in case it's needed.
            newButton.addEventListener("contextmenu", (ev) => {
                ev.preventDefault(); // Don't pop up the right click menu but ONLY on this button.
                ev.stopPropagation();
                undoLast();
            });

            copyButtonEl.parentNode.appendChild(newButton);
        }
    }

    addCodeboxButtons();
    addBareBoxButtons();

})();