Derpibooru Comment Enhancements

Improvements to Derpibooru's comment section

Versión del día 11/06/2017. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Derpibooru Comment Enhancements
// @description  Improvements to Derpibooru's comment section
// @version      1.3.1
// @author       Marker
// @namespace    https://github.com/marktaiwan/
// @homepageURL  https://github.com/marktaiwan/Derpibooru-Link-Preview
// @supportURL   https://github.com/marktaiwan/Derpibooru-Link-Preview/issues
// @include      /^https?://(www\.)?(derpibooru\.org|trixiebooru\.org)/(images/)?\d{1,}(\?|\?.{1,}|/|\.html)?$/
// @include      /^https?://(www\.)?(derpibooru\.org|trixiebooru\.org)/lists/user_comments/\d{1,}(\?|\?.{1,}|/|\.html)?$/
// @include      /^https?://(www\.)?(derpibooru\.org|trixiebooru\.org)/lists/my_comments(\?|\?.{1,}|/|\.html)?$/
// @include      /^https?://(www\.)?(derpibooru\.org|trixiebooru\.org)/(forums/)?(art|writing|dis|generals|pony|rp|meta|tagging|uppers)/?(.+)?$/
// @grant        none
// @require      https://openuserjs.org/src/libs/soufianesakhi/node-creation-observer.js
// ==/UserScript==

(function() {
    'use strict';

    const HOVER_ATTRIBUTE = 'comment-preview-active';
    var fetchCache = {};
    var backlinksCache = {};

    var formattingSyntax = {
        bold: {
            displayText: 'B',
            altText: 'bold',
            styleCSS: 'font-weight: bold;',
            options: {
                prefix: '*',
                suffix: '*'
            },
            edit: wrapSelection,
            shortcutKey: 'b'
        },
        italics: {
            displayText: 'i',
            altText: 'italics',
            styleCSS: 'font-style: italic;',
            options: {
                prefix: '_',
                suffix: '_'
            },
            edit: wrapSelection,
            shortcutKey: 'i'
        },
        under: {
            displayText: 'U',
            altText: 'underline',
            styleCSS: 'text-decoration: underline;',
            options: {
                prefix: '+',
                suffix: '+'
            },
            edit: wrapSelection,
            shortcutKey: 'u'
        },
        spoiler: {
            displayText: 'spoiler',
            altText: 'mark as spoiler',
            styleCSS: '',
            options: {
                prefix: '[spoiler]',
                suffix: '[/spoiler]'
            },
            edit: wrapSelection,
            shortcutKey: 's'
        },
        code: {
            displayText: 'code',
            altText: 'code formatting',
            styleCSS: 'font-family: "Courier New", Courier, monospace;',
            options: {
                prefix: '@',
                suffix: '@'
            },
            edit: wrapSelection
        },
        strike: {
            displayText: 'strike',
            altText: 'strikethrough',
            styleCSS: 'text-decoration: line-through;',
            options: {
                prefix: '-',
                suffix: '-'
            },
            edit: wrapSelection
        },
        superscript: {
            displayText: '<sup>sup</sup>',
            altText: 'superscript',
            options: {
                prefix: '^',
                suffix: '^'
            },
            edit: wrapSelection
        },
        subscript: {
            displayText: '<sub>sub</sub>',
            altText: 'subscript',
            options: {
                prefix: '~',
                suffix: '~'
            },
            edit: wrapSelection
        },
        link: {
            displayText: '',
            altText: 'insert hyperlink',
            styleCSS: 'background-size: 22px; background-position: center; background-repeat: no-repeat; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAALLUlEQVR4Xu2dB+x31xjHP1WjQmoTs/aoWTPErBhBxIxSe8YeMWrEHkWp2kWM2qFiREUoKTFj71GjQm1iUzMfPT9u73v3PPc950n+ed/kd8655zzf7z33Oec8z3P2IUvSGtgn6dHnwZMJkDgJMgEyARLXQOLDzzNAJkDiGkh8+HkGyARIXAOJDz/PAJkAiWsg8eHnGSATIHENJD78PANkAiSugcSHn2eA6QmgTq8IXD/8ewng7MB+wJ+BXwInAp8HTgBOmr4L3VvMBOiuq7aSFwQeBBwKHNBWuPC7RHgD8Drgjz3qTVI0E2C8Gs8FPAO4L3CGEc39FngOcBRwyoh2elXNBOilrj0K3x54FXDOcc2cpvY3gbsAX5qwzdqmMgGGafl0wBHAI4dVb631N+D+wDGtJUcWyATor0CneYE5pKXqv4GvhL+Tgb8CZw32wVWBi3d49GMC0ToUHVYkE6Cf3gT/rYBTf518F3gJ8LZg8deVuxRw92A4Nn1CnhBsg3497Vg6E6CjooKB1wT+H4DDgKOBf3Zvlv2Bp4TPSR0es5EgE6AbUm1vvobbrcP6vluLe5Y6GHgncI6aBmYhQSZAO1xt4LuOvxnw6/amWktcHjgeOF9NycltgkyAZky6gH8TwDX8VHIg8FHgvDUNaje8caqHZQLUa3IN8He9aSKBS8RrhtXFaB5kAlSrcE3wdz3yc+BMcJ6KLn4dOAj4+1gGZALsqcEYwN/16krAx4CzVQD9KODITICxGjht/ZjA3/XspsAHK4b5q7Cp5AnjYMkzwP9VFyP4u969GHhoBcoPBl4+GH3IwaFBeTGDbxf1J/hhxafg08C1MwHGaODUI9ymHT7X+VMv9Yb0+LnAYysq6ofgWcMgSf0TsBXwBfcKwFcrUL5zOHfIBOipgS2B79B8WX8CnL80To+l3SEcJKnOAFsDfwfu+4BblZB+D3CbQegnagRuFXwxfimg5V+UTwDXzQTopoF9gbc3nOfHYvDVjeZw4HGlH3Udc1dwkKT2Cbhbg5tV7OAL8IuAh5eQ/hRwnUHoJ/gJeAHgFmpZtgC+fdZfoOyNdBxwy0yAbhpwyfSWUtGtgG+3vwPoSlYUdwnLs0I3bSQ4A+jN6zSqIeX/3WOXFFOe53dWfs+CF6mJIrof8Jqebf2veGo2wG7gul2dCfjZUMWtUE/jTyOwLJcGdEQdJKkSYJCyVqx0RuB7wIVKffgWoPOILuiDJBNgkNoWr+QZgGcBZXlyCEsb3KFMgMGqW6yinkGfC9HFxYf+JfgDGG08WDIBBqtukYp6B7vTZ4h5WUadAewa2xsJ4JguE7ZHdam6aPC1P30Iz/p5MJq+ENytpnDnnoMN5w4u4o6hLI5B4+/3Yx+8NxHAU7IHAHeteWOqdPWvkKThtcA7AD1uY5Am8O3fLYAPTNHRvYEAKsvQKqNptZaHyo9DO68HJMZa0gb+s4AnTdW5rRPAbdFXAiptKvlkCNp02bW0tIEvOe89ZtlXHtBWCeCp3vNnjM/322r49yTTbEcWtYHvFrZRQX0CT1sfvUUCaMwZGtUWn+/g3Tv/WvCZM4jC+HyNQuPzTe3SJCpahZfPDlqVOqDAKuDbz60RQPAF5I4NSv4B8LLgJ6cLVZV4DnB14J7h78w15bQF7gG8aQCoXausBv7WCNAG/p8AQ6hf0TNkytWDa2rz8lTJnCRYFfwtEaAN/G8Ev7jBhyLhVNDloPn8yjIHCVYHfysEaAPf83zDp37Tdc5tKGdyRx0szlJDAu0O9wvGShv4bw6fnkkNvqpOx24DdAF/6qCN6wXrv4oE7rv7yRgDTDTgxz4D6L3rm1Bn8M3pydNEAsO0fjdwCogK/JgJsCb4O2yrSPBZ4Fp7C/ixEiAG8HcYG3jpTqNHsh8H7hWCNPtyILo3fzeA2GyAmMAvgqyehnrdRAt+bDNArOD3fduL5aMGPyYCpAi+u4vuRI5ZUYwh53/rxvAJEHy3d+9QM5o5rf3RCqxpoO3NjwL8GAiQwZ+Lgh3bXXMGyOB3BGnOYmsRIIM/J6o92l6DABn8HgDNXXRpAqQIvs4rbiCtau3XEWlJAqwJ/oVDEIXJFKa8mavN2o8a/CVXAWumZXliCJ+S7MYA3C7EA4ydXc3m/SGgym/ftqMHfykCrAn+5QCdRYri3T33GenrZ8q2dzfEH2wC/CUIsCb4ju+2wLtqXnWTQ5perc5vsKqa3kLeFGZQZpXn0Gbe/N3g5rQB1gbfMRpOrX9/XcCIkUC+rfrbm3a1zlAzK8edwgVP5Tx9RaJs5s2fmwAxgL8boxlABMZYgibRyeOLIQuHF0CZQMI0rN4DrBHZJl79araOKK39JVcBMYG/G/fNg5t4Vd79NmC7/P504Kkjjoy7PGOWMlN/AvS3142rLmhjzYMdL2r0kubBSRUrEPhpMCiXjCCalAhTE+CFDeFaa4K/U5oENdDDt9WkS0PF5AxeDvnsEf6BQ589ab0pCWCgpnnsqiQG8Iv90ijU2dRAyxuGjGFdFOv9gB7lvrrlVtAubUVRZioCGGdnHF7VFaixgV9WvBnDbhDiBbX2zcrhEs9YQjeODDUzTbs+gWtEDM9KlKkI4LUlD6zoqW+M3rWxZuGYVblbaHwKArhU8i3R+i+KSymjcE/cgiJS7eMUBDBjhUGZZRl9oVGqoCw57rEEsL5v/wGlThuk6T78pjZFllR8LM8aSwBPwr5cMZiHhWVSLONs6oc6cEloeLl38SUlYwnwkAqgDaDQkh6VwHAhFFy9HBtWAfbbxBJm3l4zSdRCQz/1MWMJcHTIzlXstDPCVRYdxbCHCf6HK/rq/kDdfsawJ0VcaywBVOCNS+Pz4MXcOjFLHfj22Zy8h8Xc+Sn7NpYA5rC9WqlDsSuwCXyHkmeAHgwzA5eRs0V5Wthr79HMYkXbwPcgy9kr2wAdITFe/hqlst7L8+iO9ZcsJvjHA1eueajhaR4U/WPJTq39rLGfgPeHvLXFcWhANaVxW2PMGfwarY8lwFGAa/6ifL9HsuYlyJDBb9DyWAL4vdTJoiyj7rGZkBUZ/BZljiWAW8Dea1+WSTNaDySEQRsuU/M3f8YZwKarloLm7LvYFBcaZPAHaqBjtbEzgI/RBtAWKMuRNbd0duza4GL5ze+huikIYAbuH4VrWYqPdm/dDJ5Ow0tJBr+npqcggI80wuZ5Fc/W116PIF2q5pa2QM0k1/ltSp+KADpZGnmrD0BZfgHcqCJGr61vfX7P4PfRVqHsVASwSU8AP1MThjUnCTL4A8G32pQEsL26fQF/86ozZwIdRaeSDP5ITU5NALvjOYD3+VSJJDh4os9BW3x+/uZ3IMccBPCxjw9RM1VdcI/As4KPdOhfXRFtjfcCl6wpkMHvqNy5CNBGApeI7hMYVNkn9brGpm5ozwTq7vnJ4HcEfw4boPzoppnAss4GZuM+Bvh2Q78vEAJO3XQqeyAXq2Xwe4C/BAF8Rt3V5+WungQYRua/euganmWCBz2PD+wwLl3RjPVL6jy/g14ai8z5CSg+2DRpvuljrnZtGsjhgMmgkvHkGQv8rv5SBPB5+g4aWXvZqToPuL9gVg4NwiwDNLAkAeyeaVceEbxuvXtnqJwS7gfUiJzitrCh/dh8vaUJsFPY/iF7pvny+8QQ6Hvgt97Pycmb134EA1iLAMWha9W7Q3hQcCXbxef7lhuqJegGm5wQNpCGXt0Sgbrj60IMBIhPKwn1KBMgIbCrhpoJkAmQuAYSH36eATIBEtdA4sPPM0AmQOIaSHz4eQbIBEhcA4kPP88AmQCJayDx4ecZIBMgcQ0kPvw8AyROgP8APpE7nxKpeWcAAAAASUVORK5CYII=);',
            options: {
                prefix: '"',
                suffix: '":',
                insertLink: true
            },
            edit: wrapSelection
        },
        image: {
            displayText: '',
            altText: 'insert image',
            styleCSS: 'background-size: 20px; background-position: center; background-repeat: no-repeat; background-image: url(data:image/png;base64,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);',
            options: {
                prefix: '!',
                suffix: '!',
                insertImage: true
            },
            edit: wrapSelection
        },
        no_parse: {
            displayText: 'no parse',
            altText: 'Text you want the parser to ignore',
            options: {
                prefix: '[==',
                suffix: '==]'
            },
            edit: wrapSelection
        }
    };

    function displayHover(comment, sourceLink) {
        const PADDING = 5; // in pixels

        comment = comment.cloneNode(true);
        comment.id = 'hover_preview';
        comment.style.position = 'absolute';
        comment.style.maxWidth = '980px';
        comment.style.minWidth = '490px';
        comment.style.boxShadow = '0px 0px 12px 0px rgba(0, 0, 0, 0.4)';

        // Make spoiler visible
        var i;
        var list = comment.querySelectorAll('span.spoiler, span.imgspoiler, span.imgspoiler img');
        if (list !== null) {
            for (i = 0; i < list.length; i++) {

                if (list[i].matches('span')) {
                    list[i].style.color = '#333';
                    list[i].style.backgroundColor = (list[i].matches('span.imgspoiler')) ? '' : '#f7d4d4';
                } else {
                    list[i].style.visibility = 'visible';
                }

            }
        }

        // highlight reply link
        var ele = sourceLink;
        while (ele.parentElement !== null && !ele.matches('article')) ele = ele.parentElement;
        var sourceCommentID = ele.id.slice(8);

        list = comment.querySelectorAll('a[href$="#comment_' + sourceCommentID + '"]');
        if (list !== null) {
            for (i = 0; i < list.length; i++) list[i].style.textDecoration = 'underline dashed';
        }

        // relative time
        window.booru.timeAgo(comment.querySelectorAll('time'));

        var container = document.getElementById('image_comments') || document.getElementById('content');
        if (container) container.appendChild(comment);

        // calculate link position
        var linkRect = sourceLink.getBoundingClientRect();
        var linkTop = linkRect.top + viewportPosition().top;
        var linkLeft = linkRect.left + viewportPosition().left;

        var commentRect = comment.getBoundingClientRect();
        var commentTop;
        var commentLeft;


        if (sourceLink.parentElement.classList.contains('comment_backlinks')) {
            // When there is room, place the preview below the link,
            // otherwise place it above the link
            if (document.documentElement.clientHeight - linkRect.bottom > commentRect.height + PADDING) {

                commentTop = linkTop + linkRect.height + PADDING;
                commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? (
                    linkLeft
                ) : (
                    document.documentElement.clientWidth - (commentRect.width + PADDING)
                );

            } else {

                commentTop = linkTop - commentRect.height - PADDING;
                commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? (
                    linkLeft
                ) : (
                    document.documentElement.clientWidth - (commentRect.width + PADDING)
                );

            }
        } else {
            // When there is room, place the preview above the link
            // otherwise place it to the right and aligns it to the top of viewport
            if (linkRect.top > commentRect.height + PADDING) {

                commentTop = linkTop - commentRect.height - PADDING;
                commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? (
                    linkLeft
                ) : (
                    document.documentElement.clientWidth - (commentRect.width + PADDING)
                );

            } else {

                commentTop = viewportPosition().top + PADDING;
                commentLeft = linkLeft + linkRect.width + PADDING;

            }
        }

        comment.style.top = commentTop + 'px';
        comment.style.left = commentLeft + 'px';
    }

    function linkEnter(sourceLink, targetCommentID) {

        sourceLink.setAttribute(HOVER_ATTRIBUTE, 1);

        var targetComment = document.getElementById('comment_' + targetCommentID);

        if (targetComment !== null) {

            if (!elementInViewport(targetComment)) {
                displayHover(targetComment, sourceLink);
            }

            // Highlight linked post
            targetComment.children[0].style.backgroundColor = 'rgba(230,230,30,0.3)';
            if (targetComment.querySelector('.comment_backlinks') !== null) targetComment.children[1].style.backgroundColor = 'rgba(230,230,30,0.3)';

        } else {

            // External post, display from cached response if possible
            if (fetchCache[targetCommentID] !== undefined) {

                displayHover(fetchCache[targetCommentID], sourceLink);

            } else {

                fetch(window.location.origin + '/comment/' + targetCommentID + '.html')
                    .then((response) => response.text())
                    .then((text) => {
                        if (fetchCache[targetCommentID] === undefined && sourceLink.getAttribute(HOVER_ATTRIBUTE) !== '0') {
                            var d = document.createElement('div');
                            d.innerHTML = text;
                            fetchCache[targetCommentID] = d.firstChild;
                            displayHover(d.firstChild, sourceLink);
                        }
                    });

            }

        }
    }

    function linkLeave(sourceLink, targetCommentID) {

        sourceLink.setAttribute(HOVER_ATTRIBUTE, 0);
        var targetComment = document.getElementById('comment_' + targetCommentID);
        var preview = document.getElementById('hover_preview');

        if (targetComment !== null) {
            targetComment.children[0].style.backgroundColor = '';  //remove comment highlight
            if (targetComment.querySelector('.comment_backlinks') !== null) targetComment.children[1].style.backgroundColor = '';
        }
        if (preview !== null) preview.parentElement.removeChild(preview);

    }

    // Chrome/Firefox compatibility hack for getting viewport position
    function viewportPosition() {
        return {
            top: (document.documentElement.scrollTop || document.body.scrollTop),
            left: (document.documentElement.scrollLeft || document.body.scrollLeft)
        };
    }

    function elementInViewport(el) {
        var rect = el.getBoundingClientRect();

        return (
            rect.top + (rect.height - 50) >= 0 &&
            rect.bottom - (rect.height - 50) <= document.documentElement.clientHeight
        );
    }

    function createBacklinksContainer(commentBody) {
        var ele = commentBody.querySelector('div.comment_backlinks');

        if (ele === null) {

            ele = document.createElement('div');
            ele.className = 'block__content comment_backlinks';
            ele.style.fontSize = '12px';
            ele.style.borderTop = window.getComputedStyle(commentBody.firstChild)['border-top'];

            commentBody.insertBefore(ele, commentBody.querySelector('.communication__options'));

        }
        return ele;
    }

    function insertBacklink(backlink, commentID) {

        // add to cache
        if (backlinksCache[commentID] === undefined) backlinksCache[commentID] = [];
        if (backlinksCache[commentID].findIndex((ele) => (ele.hash == backlink.hash)) == -1) {
            backlinksCache[commentID].push(backlink);
        }

        var commentBody = document.getElementById('comment_' + commentID);
        if (commentBody !== null) {
            var linksContainer = createBacklinksContainer(commentBody);

            // insertion sort the links so they are ordered by id
            if (linksContainer.children.length > 0) {

                var iLinkID = parseInt(backlink.hash.slice(9), 10);
                var iTempID;
                var i;

                for (i = 0; i < linksContainer.children.length; i++) {
                    iTempID = parseInt(linksContainer.children[i].hash.slice(9), 10);

                    if (iLinkID == iTempID) {  // prevent links to the same comment from being added multiple times
                        return;
                    }
                    if (iLinkID < iTempID) {
                        linksContainer.insertBefore(backlink, linksContainer.children[i]);
                        return;
                    }
                }

            }
            linksContainer.appendChild(backlink);
            return;
        }

    }

    function getQueryVariable(key, HTMLAnchorElement) {
        var i;
        var array = HTMLAnchorElement.search.substring(1).split('&');

        for (i = 0; i < array.length; i++) {
            if (key == array[i].split('=')[0]) {
                return array[i].split('=')[1];
            }
        }
    }

    function insertButton(displayText) {

        var commentsBlock = document.querySelector('.js-editable-comments');

        var ele = document.createElement('div');
        ele.className = 'block__header';
        ele.id = 'comment_loading_button';
        ele.style.textAlign = 'center';

        ele.appendChild(document.createElement('a'));
        ele.firstChild.style.padding = '0px';
        ele.firstChild.style.width = '100%';
        ele.firstChild.innerText = displayText;

        commentsBlock.insertBefore(ele, commentsBlock.lastElementChild);

        return ele;
    }

    function loadComments(e, imageId, nextPage, lastPage) {

        e.target.parentElement.remove();
        var btn = insertButton('Loading...');

        var fetchURL = window.location.origin + '/images/' + imageId + '/comments?id=' + imageId + '&page=' + nextPage;

        fetch(fetchURL, {credentials: 'same-origin'})  // cookie needed for correct pagination
            .then((response) => response.text())
            .then((text) => {

                // response text => documentFragment
                var ele = document.createElement('div');
                var range = document.createRange();

                ele.innerHTML = text;
                range.selectNodeContents(ele.firstChild);

                var fragment = range.extractContents();
                var commentsBlock = document.getElementById('image_comments');

                // update pagination blocks
                commentsBlock.replaceChild(fragment.firstChild, commentsBlock.firstElementChild);
                commentsBlock.replaceChild(fragment.lastChild, commentsBlock.lastElementChild);

                // page marker
                ele.innerHTML = '';
                ele.className = 'block block__header';
                ele.style.textAlign = 'center';
                ele.innerText = 'Page ' + nextPage;

                // relative time
                window.booru.timeAgo(fragment.querySelectorAll('time'));

                fragment.insertBefore(ele, fragment.firstElementChild);
                commentsBlock.insertBefore(fragment, commentsBlock.lastElementChild);


                // configure button to load the next batch of comments
                btn.remove();
                if (nextPage < lastPage) {
                    btn = insertButton('Load more comments');

                    btn.addEventListener('click', (e) => {
                        loadComments(e, imageId, nextPage + 1, lastPage);
                    });
                }

            });
    }

    function wrapSelection(box, options) {
        if (box === null) {
            return;
        }
        var hyperlink;
        var prefix = options.prefix;
        var suffix = options.suffix;

        // record scroll top to restore it later.
        var scrollTop = box.scrollTop;
        var selectionStart = box.selectionStart;
        var selectionEnd = box.selectionEnd;
        var text = box.value;
        var beforeSelection = text.substring(0, selectionStart);
        var selectedText = text.substring(selectionStart, selectionEnd);
        var afterSelection = text.substring(selectionEnd);

        var emptySelection = (selectedText === '');

        var trailingSpace = '';
        var cursor = selectedText.length - 1;

        // deselect trailing space and carriage return
        while (cursor > 0 && (selectedText[cursor] === ' ' || selectedText[cursor] === '\n')) {
            trailingSpace = selectedText[cursor] + trailingSpace;
            cursor--;
        }
        selectedText = selectedText.substring(0, cursor + 1);

        if (options.insertLink) {
            hyperlink = window.prompt('Enter link:');
            if (hyperlink === null || hyperlink === '') return;
            // change on-site link to use relative url
            if (hyperlink.startsWith(window.origin)) {
                hyperlink = hyperlink.substring(window.origin.length);
            }
            suffix += hyperlink;
        }

        if (options.insertImage) {
            hyperlink = window.prompt('Enter link to image:');
            if (hyperlink === null || hyperlink === '') return;
            // change on-site image to embed
            var resultsArray = hyperlink.match(/https?:\/\/(?:www\.)?(?:(?:derpibooru\.org|trixiebooru\.org)\/(?:images\/)?(\d{1,})(?:\?|\?.{1,}|\/|\.html)?|derpicdn\.net\/img\/(?:view\/)?\d{1,}\/\d{1,}\/\d{1,}\/(\d+))/i);
            if (resultsArray !== null) {
                var imageId = resultsArray[1] || resultsArray[2];
                if (imageId === undefined) {
                    console.error('Derpibooru Comment Preview: Unable to extract image ID from link: ' + hyperlink);
                    return;
                }
                prefix = '>>';
                selectedText = imageId;
                suffix = 'p';
            } else {
                selectedText = hyperlink;
            }
            emptySelection = false;
        }

        box.value = beforeSelection + prefix + selectedText + suffix + trailingSpace + afterSelection;
        if (emptySelection) {
            box.selectionStart = beforeSelection.length + prefix.length;
        } else {
            box.selectionStart = beforeSelection.length + prefix.length + selectedText.length + suffix.length;
        }
        box.selectionEnd = box.selectionStart;
        box.scrollTop = scrollTop;
    }

    function initToolbar(formats) {
        var commentBox = document.querySelector('.js-preview-input');
        if (commentBox === null) {
            return;
        }
        var toolbar = document.createElement('div');
        var stringBuilder = [];
        var ele;

        // CSS
        var styleElement = document.createElement('style');
        styleElement.id = 'dce-css';
        styleElement.type = 'text/css';
        stringBuilder.push('/* Generated by Derpibooru Comment Preview */');
        stringBuilder.push('#dce-toolbar {margin-bottom: 6px;}');
        stringBuilder.push('#dce-toolbar .button {height: 28px; min-width: 28px; text-align: center; vertical-align: middle; margin: 2px;}');
        for (ele in formats) {
            if (formats[ele].styleCSS !== undefined) {
                stringBuilder.push(`#dce-toolbar .button[formatting="${ele}"] {${formats[ele].styleCSS}}`);
            }
        }
        styleElement.innerHTML = stringBuilder.join('\n');
        document.head.appendChild(styleElement);

        // HTML
        for (ele in formats) {
            if (formats[ele].displayText !== null) {
                var btn = document.createElement('div');
                var name = formats[ele].displayText;
                var altText = formats[ele].altText;
                var key = formats[ele].shortcutKey;

                btn.className = 'button';
                btn.innerHTML = name;
                if (altText !== undefined) {
                    if (key !== undefined) {
                        altText += ` (ctrl+${key})`;
                    }
                    btn.title = altText;
                }
                btn.setAttribute('formatting', ele);

                toolbar.appendChild(btn);
            }
        }
        toolbar.id = 'dce-toolbar';
        commentBox.parentElement.insertBefore(toolbar, commentBox);

        // Event listeners
        toolbar.addEventListener('click', function (e) {
            if (e.target.matches('.button') || e.target.parentElement.matches('.button')) {
                var ele;
                var btn = (e.target.matches('.button')) ? e.target : e.target.parentElement;
                var box = document.querySelector('.js-preview-input');

                for (ele in formattingSyntax) {
                    if (btn.getAttribute('formatting') == ele) {
                        formattingSyntax[ele].edit(box, formattingSyntax[ele].options);
                    }
                }
                box.focus();
            }
        });
        commentBox.addEventListener('keydown', function (e) {
            if (e.ctrlKey && !e.shiftKey) {
                var ch = e.key.toLowerCase();
                var formats = formattingSyntax;
                var ele;
                var box = e.target;

                for (ele in formats) {
                    if (ch == formats[ele].shortcutKey) {
                        formats[ele].edit(box, formats[ele].options);
                        e.preventDefault();
                    }
                }
            }
        });
    }

    NodeCreationObserver.onCreation('article[id^="comment_"]', function (sourceCommentBody) {

        var links = sourceCommentBody.querySelectorAll('.communication__body__text a[href*="#comment_"]');
        var sourceCommentID = sourceCommentBody.id.slice(8);

        var ele = sourceCommentBody.querySelector('.communication__body__sender-name');
        var sourceAuthor = (ele.firstElementChild !== null && ele.firstElementChild.matches('a')) ? ele.firstElementChild.innerText : ele.innerHTML;

        links.forEach((link) => {

            var targetCommentID = link.hash.slice(9);    // Example: link.hash == "#comment_5430424"
            var backlink;

            // add backlink if the comment is not part of a quote
            // and not fetched
            if (!link.matches('blockquote a') && !sourceCommentBody.matches('.fetched-comment')) {

                backlink = document.createElement('a');

                backlink.style.marginRight = '5px';
                backlink.href = '#comment_' + sourceCommentID;
                backlink.textContent = '►';
                backlink.innerHTML += sourceAuthor;

                backlink.addEventListener('mouseenter', () => {
                    linkEnter(backlink, sourceCommentID);
                });
                backlink.addEventListener('mouseleave', () => {
                    linkLeave(backlink, sourceCommentID);
                });
                backlink.addEventListener('click', () => {
                    // force pageload instead of trying to navigate to a nonexistent anchor on the current page
                    if (document.getElementById('comment_' + sourceCommentID) === null) window.location.reload();
                });

                insertBacklink(backlink, targetCommentID);
            }


            // ignore quotes
            // this is terrible
            if (link.nextElementSibling &&
                    link.nextElementSibling.nextElementSibling &&
                    link.nextElementSibling.nextElementSibling.matches('blockquote')) return;

            link.addEventListener('mouseenter', () => {
                linkEnter(link, targetCommentID);
            });
            link.addEventListener('mouseleave', () => {
                linkLeave(link, targetCommentID);
            });

        });

        // If other pages had replied to this comment
        if (backlinksCache[sourceCommentID] !== undefined) {
            backlinksCache[sourceCommentID].forEach((backlink) => {
                insertBacklink(backlink, sourceCommentID);
            });
        }

    });

    // Comment loading
    NodeCreationObserver.onCreation('#image_comments nav>a.js-next', function (btnNextPage) {

        if (document.getElementById('comment_loading_button') !== null) return;

        var btnLastPage = btnNextPage.nextElementSibling;

        var imageId = getQueryVariable('id', btnNextPage);
        var nextPage = parseInt(getQueryVariable('page', btnNextPage), 10);
        var lastPage = parseInt(getQueryVariable('page', btnLastPage), 10);

        var btn = insertButton('Load more comments');

        btn.addEventListener('click', (e) => {
            loadComments(e, imageId, nextPage, lastPage);
        });

    });

    initToolbar(formattingSyntax);
})();