Board Games SE mana symbols

Convert mana symbols to MTG's icons on MTG questions.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Board Games SE mana symbols
// @author      doppelgreener
// @namespace   https://greasyfork.org/en/users/5615-doppelgreener
// @description Convert mana symbols to MTG's icons on MTG questions.
// @supportURL  http://meta.boardgames.stackexchange.com/questions/1245/
// @grant       none
// @include     http://boardgames.stackexchange.com/questions/*
// @include     https://boardgames.stackexchange.com/questions/*
// @include     http://boardgames.meta.stackexchange.com/questions/*
// @include     https://boardgames.meta.stackexchange.com/questions/*
// @version     1.11.1
// ==/UserScript==

(function () {

    // For those wondering, this is called a protocol-relative URL.
    var spriteSheetUri = "//i.imgur.com/sQZMBlp.png";
    // This sprite sheet is double size to support screen zooming and high-res screens.
    // Part of this trick requires us overriding the size to half-size it.
    var spriteSheetSizeOverride = "169px";
    var symbolSizePx = 16;
    var symbolSpacingPx = 1;

    var manaSymbols = [
        {letter: '0', x: 0, y: 0, cls: 'n0'},
        {letter: '1', x: 1, y: 0, cls: 'n1'},
        {letter: '2', x: 2, y: 0, cls: 'n2'},
        {letter: '3', x: 3, y: 0, cls: 'n3'},
        {letter: '4', x: 4, y: 0, cls: 'n4'},
        {letter: '5', x: 5, y: 0, cls: 'n5'},
        {letter: '6', x: 6, y: 0, cls: 'n6'},
        {letter: '7', x: 7, y: 0, cls: 'n7'},
        {letter: '8', x: 8, y: 0, cls: 'n8'},
        {letter: '9', x: 9, y: 0, cls: 'n9'},
        {letter: '10', x: 0, y: 1, cls: 'n10'},
        {letter: '11', x: 1, y: 1, cls: 'n11'},
        {letter: '12', x: 2, y: 1, cls: 'n12'},
        {letter: '13', x: 3, y: 1, cls: 'n13'},
        {letter: '14', x: 4, y: 1, cls: 'n14'},
        {letter: '15', x: 5, y: 1, cls: 'n15'},
        {letter: '16', x: 6, y: 1, cls: 'n16'},
        {letter: '17', x: 7, y: 1, cls: 'n17'},
        {letter: '18', x: 8, y: 1, cls: 'n18'},
        {letter: '19', x: 9, y: 1, cls: 'n19'},
        {letter: '20', x: 0, y: 2, cls: 'n20'},
 
        {letter: 'X', x: 1, y: 2, cls: 'X'},
        {letter: 'Y', x: 2, y: 2, cls: 'Y'},
        {letter: 'Z', x: 3, y: 2, cls: 'Z'},
 
        {letter: 'W', x: 4, y: 2, cls: 'W'},
        {letter: 'U', x: 5, y: 2, cls: 'U'},
        {letter: 'B', x: 6, y: 2, cls: 'B'},
        {letter: 'R', x: 7, y: 2, cls: 'R'},
        {letter: 'G', x: 8, y: 2, cls: 'G'},
        {letter: 'S', x: 9, y: 2, cls: 'S'},
 
        // Hybrid mana proper aliases
        {letter: 'W/U', x: 0, y: 3, cls: 'WU'},
        {letter: 'W/B', x: 1, y: 3, cls: 'WB'},
        {letter: 'U/B', x: 2, y: 3, cls: 'UB'},
        {letter: 'U/R', x: 3, y: 3, cls: 'UR'},
        {letter: 'B/R', x: 4, y: 3, cls: 'BR'},
        {letter: 'B/G', x: 5, y: 3, cls: 'BG'},
        {letter: 'R/W', x: 6, y: 3, cls: 'RW'},
        {letter: 'R/G', x: 7, y: 3, cls: 'RG'},
        {letter: 'G/W', x: 8, y: 3, cls: 'GW'},
        {letter: 'G/U', x: 9, y: 3, cls: 'GU'},
 
        // Hybrid mana reverse aliases
        {letter: 'U/W', x: 0, y: 3, cls: 'WU'},
        {letter: 'B/W', x: 1, y: 3, cls: 'WB'},
        {letter: 'B/U', x: 2, y: 3, cls: 'UB'},
        {letter: 'R/U', x: 3, y: 3, cls: 'UR'},
        {letter: 'R/B', x: 4, y: 3, cls: 'BR'},
        {letter: 'G/B', x: 5, y: 3, cls: 'BG'},
        {letter: 'W/R', x: 6, y: 3, cls: 'RW'},
        {letter: 'G/R', x: 7, y: 3, cls: 'RG'},
        {letter: 'W/G', x: 8, y: 3, cls: 'GW'},
        {letter: 'U/G', x: 9, y: 3, cls: 'GU'},
 
        // 2/x hybrid mana
        {letter: '2/W', x: 0, y: 4, cls: 'TW'},
        {letter: '2/U', x: 1, y: 4, cls: 'TU'},
        {letter: '2/B', x: 2, y: 4, cls: 'TB'},
        {letter: '2/R', x: 3, y: 4, cls: 'TR'},
        {letter: '2/G', x: 4, y: 4, cls: 'TG'},
 
        // Phyrexian mana
        {letter: 'WP', x: 5, y: 4, cls: 'WP'},
        {letter: 'UP', x: 6, y: 4, cls: 'UP'},
        {letter: 'BP', x: 7, y: 4, cls: 'BP'},
        {letter: 'RP', x: 8, y: 4, cls: 'RP'},
        {letter: 'GP', x: 9, y: 4, cls: 'GP'},
 
        // Tap and untap
        {letter: 'T', x: 0, y: 5, cls: 'T'},
        {letter: 'Q', x: 1, y: 5, cls: 'Q'},

        // Colorless, Energy, raw Phyrexian symbol
        {letter: 'C', x: 2, y: 5, cls: 'C'},
        {letter: 'E', x: 3, y: 5, cls: 'E'},
        {letter: 'P', x: 4, y: 5, cls: 'P'}
    ];

    // Adds derivative data to each mana symbol entry.
    function primeManaSymbols() {
        for (var key in manaSymbols) {
            if (manaSymbols.hasOwnProperty(key)) {
                var symbol = manaSymbols[key];
                symbol.fullText = '{' + symbol.letter + '}';
            }
        }
    }

    // Creates the page's style sheet.
    function createStyleSheet() {
        var css = ".mana-symbol { display: inline-block; width: 16px; height: 16px; background: url('$SPRITES') no-repeat black; background-size: $SIZE; position: relative; top: 0.2em; box-shadow: 1px 1px 0px 0px #000; border-radius: 8px }"
            .replace('$SIZE', spriteSheetSizeOverride)
            .replace('$SPRITES', spriteSheetUri);
        css += "\n.mana-symbol + .mana-symbol { margin-left: 1px; }";
        css += "\n.mana-symbol.E { border-radius: 0; background-color: transparent; box-shadow: none; }";
        css += "\n.mana-symbol.P { border-radius: 0; background-color: transparent; box-shadow: none; }";
        css += "\n.mana-symbol .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }";

        var template = "\n.mana-symbol.$CLASS { background-position: $Xpx $Ypx }";

        var classesCreated = [];

        for (var key in manaSymbols) {
            if (manaSymbols.hasOwnProperty(key)) {
                var symbol = manaSymbols[key];

                if (classesCreated.indexOf(symbol.cls) >= 0) continue;
                classesCreated.push(symbol.cls);

                var xPos = -1 * (symbolSizePx + symbolSpacingPx) * symbol.x;
                var yPos = -1 * (symbolSizePx + symbolSpacingPx) * symbol.y;

                css += template
                    .replace('$CLASS', symbol.cls)
                    .replace('$X', xPos)
                    .replace('$Y', yPos);
            }
        }

        var styleElem = $('<style>').attr('id', 'mana-symbols-styling').text(css);
        $('head').append(styleElem);
    }

    var converter = (function () {
        // Creates a jQuery element representing a mana symbol.
        function createSymbolElement(symbol) {
            var symbolElement = $('<span>')
                .addClass('mana-symbol')
                .addClass(symbol.cls);

            var textAlternative = $('<span>')
                .addClass('sr-only')
                .text(symbol.fullText);

            symbolElement.append(textAlternative);

            return symbolElement;
        }

        // Adds mana symbols within the given text node.
        function processNode(node, nodeList) {
            // Here's a basic overview of how node processing works:
            // 1. Go through each text node on the document.
            //      jQuery offers us no utilities for doing this,
            //      so we're working directly with the DOM.
            // 2. Find any mention of a symbol, e.g.:
            //      "{T}: Add {G}{G} to your mana pool."
            // 3. Split the thing on the current symbol:
            //      "{T} Add ", "", " to your mana pool."
            // 4. Set the text node we were looking at to the final value in that list.
            // 5. Insert the current symbol before it.
            // 6. Insert the previous text node before that text node.
            // 7. Repeat for other text nodes.
            // 8. Repeat 2-7 for other symbols.
            if (node.nodeValue.indexOf('{') < 0) {
                return;
            }

            for (var key in manaSymbols) {
                var symbol = manaSymbols[key];
                var symbolText = symbol.fullText;
                var parts = node.nodeValue.split(symbolText);

                if (parts.length <= 1) continue;

                // Set the node to the value of the last part. We'll be inserting stuff before it.
                node.nodeValue = parts.pop();

                // Work backwards inserting new nodes, and this symbol between each.
                var lastNode = node;
                var parent = node.parentNode;
                var i = parts.length;
                while (i > 0) {
                    i--;
                    var part = parts[i];
                    var currentNode = document.createTextNode(part);
                    var htmlSymbol = createSymbolElement(symbol)[0];
                    parent.insertBefore(htmlSymbol, lastNode);
                    parent.insertBefore(currentNode, htmlSymbol);
                    nodeList.push(currentNode);
                    lastNode = currentNode;
                }
            }
        }

        return {
            processNode : processNode
        };
    })();

    var htmlParsing = (function () {
        // Returns all text nodes inside the given node (including the node itself).
        function findTextNodes(node) {
            var textNodes = [];

            var isCodeElement = node.nodeName && (node.nodeName.toUpperCase() === "CODE");
            if (isCodeElement) return textNodes;

            var isAlreadyParsed = (node.nodeType === Node.ELEMENT_NODE) && $(node).hasClass('mana-symbol');
            if (isAlreadyParsed) return textNodes;

            if (node.nodeType == Node.TEXT_NODE) {
                textNodes.push(node);
            } else {
                for (var i in node.childNodes) {
                    var child = node.childNodes[i];
                    var contents = findTextNodes(child);
                    textNodes = textNodes.concat(contents);
                }
            }
            return textNodes;
        }

        // Indicates whether a node has any pattern resembling a mana symbol.
        function mayContainManaSymbols(node) {
            var symbolRegex = /\{[0-9A-Z\/]{1,3}\}/;
            return ($(node).text().search(symbolRegex) >= 0);
        }

        // Checks for mana symbols in a DOM node and has them replaced.
        function prettify(node) {
            if (!mayContainManaSymbols(node)) return;
            var nodeList = findTextNodes(node);
            while (nodeList.length > 0) {
                var childNode = nodeList.shift();
                converter.processNode(childNode, nodeList);
            }
        }

        return {
            prettify: prettify
        };
    })();

    function viewingMtgQuestion() {
        var questionTags = $('.post-taglist', '#question');
        return $(".post-tag:contains('magic-the-gathering')", questionTags).length > 0;
    }

    function replaceInPosts() {
        $('.post-text').each(function() {
           htmlParsing.prettify(this);
        });
    }

    // Post preview replacement method thanks to Ilmari Karonen:
    // http://stackapps.com/a/6149/
    function replaceInEditorPreview() {
        function parse(text) {
            var content = $('<div>').html(text);
            htmlParsing.prettify(content[0]);
            return content.html();
        }

        StackExchange.ifUsing('editor', function () {
            StackExchange.MarkdownEditor.creationCallbacks.add(function (editor) {
                editor.getConverter().hooks.chain('postConversion', parse);
            });
        });
    }

    // Post editing replacement method thanks to Ilmari Karonen:
    // http://stackapps.com/a/6149/
    function replaceAfterChangeMade() {
        var urlRegex = /^\/posts\/(ajax-load-realtime|\d+\/edit-submit)\/|^\/review\/(next-task|task-reviewed)\b/;
        $(document).ajaxComplete(function (event, xhr, settings) {
            if (urlRegex.test(settings.url)) replaceInPosts();
        });
    }

    if (typeof StackExchange !== 'undefined') {
        StackExchange.ready(function() {
            if (viewingMtgQuestion()) {
                primeManaSymbols();
                createStyleSheet();

                replaceInPosts();
                replaceInEditorPreview();
                replaceAfterChangeMade();
            }
        });
    }

})();