// ==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();
}
});
}
})();