MediaWiki:ChatLinkSearch.js

From Guild Wars 2 Wiki
Jump to navigationJump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* <nowiki> */
/**
 * GW2W Chat link search
 *
 * Decodes Guild Wars 2 chat links in the search panel, and tries to find the
 * corresponding article using the SMW property "Has game id".
 *
 * Original by Patrick Westerhoff [User:Poke]. 2022 modifications by Chieftain Alex.
 */
 
 
/**
 * Since square brackets are considered illegal characters for native interwiki search redirects,
 * this JS is required such that interwiki's can be respected when searching from the ingame
 * /wiki command with a chatlink.
 *
 * "de:" redirects to the german wiki,
 * "fr:" redirects to the french wiki,
 * "es:" redirects to the spanish wiki.
 * 
 * Based on a suggestion at [[MediaWiki talk:ChatLinkSearch.js]] by [[de:Benutzer:Olertu]]
 *
 * To disable this functionality by setting a cookie, visit [[Widget:No interwiki search]].
 */
 (function checkForInterWiki() {
    var searchBar = document.querySelector('#searchText input');
    if (!searchBar) {
        return;
    }

    // Check for a cookie which, if set, prevents the wiki redirecting on chat links
    var stopCookie = getCookie('ignoreInterwikiSearchRedirect');

    // Check for an interwiki prefix
    var match = searchBar.value.match(/^(de|fr|es):(.*?)$/i);
    if (match && stopCookie === null) {
        console.log('Redirecting from ',window.location.href,'  to another language wiki.');
        window.location.href = 'https://wiki-' + match[1] + '.guildwars2.com/index.php?title=Special:Search&search=' + encodeURIComponent(match[2]);
    } else {
        chatLinkSearch(searchBar)
    }
})();

function getCookie(k) {
    var v = document.cookie.match('(^|;) ?' + k + '=([^;]*)(;|$)');
    return v ? v[2] : null
}

function chatLinkSearch(searchBar) {
    var mwApi;
    
    function convertWikiMarkupToHTML(data, element_id) {
        // Remove the header from the parsed payload, cleanup rest
        var headertype = data.headername;
        delete data.headername;
        delete data.header;
        delete data.extendedoutput;
        delete data.searchflag;
        
        var parse_payload = 'WIDGETSEPARATOR' + '{{Template:ChatLinkSearch ' + headertype + '|' + $.map(data, function(v,k){
            return k + '=' + v;
        }).join('|') + '}}' + 'WIDGETSEPARATOR';

        mw.loader.using('mediawiki.api', function () {
            var api = new mw.Api();
            api.parse(parse_payload)
            .done(function (result) {
                var parsed_payload = result.split('WIDGETSEPARATOR');

                // Remove first two elements where the header and footer of the parsed data will be (div open, div closed+pp limit report)
                parsed_payload.pop();
                parsed_payload.shift();

                // Distribute result back to source
                $('#' + element_id).html(parsed_payload[0]);
            })
            .fail(function(d, textStatus, error) {
                console.log('[[MediaWiki:ChatLinkSearch.js]]: Mediawiki API failed to parse text.');
                console.error('[[MediaWiki:ChatLinkSearch.js]]: GW2W API Parse operation failed, status: ' + textStatus + ', error: '+error);
            });
        });
    }

    // Helper function: Convert item mask into options (upgrades, sigils/runes, skins)
    function itemChoices(mask){
        var option = {};
        // Bitmask meanings: 0 = no upgrades, 64 (or 32) = 1 sigil, 96 = 2 sigils, 128 = skinned, 192 (or 160) = skinned + 1 sigil, 224 = skinned + 2 sigils
        switch (mask) {
            case 0:   option.name = 'no upgrades';                         option.arr = ['','',''];             break;
            case 32:
            case 64:  option.name = 'one sigil/rune';                      option.arr = ['item','',''];         break;
            case 96:  option.name = 'two sigils/runes';                    option.arr = ['item','item',''];     break;
            case 128: option.name = 'skin applied';                        option.arr = ['skin','',''];         break;
            case 160:
            case 192: option.name = 'one sigil/rune and a skin applied';   option.arr = ['skin','item',''];     break;
            case 224: option.name = 'two sigils/runes and a skin applied'; option.arr = ['skin','item','item']; break;
            default:  option.name = 'unknown';                             option.arr = ['','',''];             break;
        }
        return option;
    }

    // Helper function: Convert specialization mask into options (unallocated 0/top 1/middle 2/bottom 3)
    function specializationChoices(mask8){
        // Convert to binary
        var binary = mask8.toString(2).padStart(8,'0');

        // Split into pairs
        var binary_pairs = binary.match(/../g);

        // Remove the useless 1st pair
        binary_pairs.shift();

        // Reverse the order and convert back into decimals
        var positions = $.map(binary_pairs.reverse(), function(v) {
            return parseInt(v,2);
        });
        return positions;
    }
    
    // Helper function: Convert item visibility mask (length 16 binary bits) into slots. Status is 1 if visible, 0 if hidden.
    function itemVisibility(mask16){
        var bitmask = mask16.toString(2).padStart(16,'0').match(/./g).reverse();
        var slots = ['aquabreather', 'back', 'coat', 'boots', 'gloves', 'helm', 'legs', 'shoulders', 'outfit', 'aquaweapon1', 'aquaweapon2', 'weapon1', 'weapon2', 'weapon3', 'weapon4'];
        
        // Only return data on hidden slots
        var hiddenslots = [];
        $.map(slots, function(v,k){ if (bitmask[k] == 0) { hiddenslots.push(v); } });
        return hiddenslots;
    }
    
    // Helper function: Convert travel visibility mask ??
    function travelVisibility(mask16){
        var bitmask = mask16.toString(2).padStart(16,'0').match(/./g).reverse();
        return bitmask;
    }
    
    // Helper function: Convert weapon type numbers into type names
    function weaponTypeNames(id){
        var name;
        switch (id) {
            case 5:   name = "Axe";        break;
            case 35:  name = "Longbow";    break;
            case 47:  name = "Dagger";     break;
            case 49:  name = "Focus";      break;
            case 50:  name = "Greatsword"; break;
            case 51:  name = "Hammer";     break;
            case 53:  name = "Mace";       break;
            case 54:  name = "Pistol";     break;
            case 85:  name = "Rifle";      break;
            case 86:  name = "Scepter";    break;
            case 87:  name = "Shield";     break;
            case 89:  name = "Staff";      break;
            case 90:  name = "Sword";      break;
            case 102: name = "Torch";      break;
            case 103: name = "Warhorn";    break;
            case 107: name = "Shortbow";   break;
            case 265: name = "Spear";      break;
            default: "(unknown weapon type " + id + ")";
        }
        return name;
    }
    
    // Helper function: Reads a string like AAB60000, break into pairs AA-B6-00-00, reverses pairs 00-00-B6-AA, joins, and converts HEX (radix 16) to DECIMAL (radix 10).
    // Note: prefixing a number with 0x would allow you to skip specifying the 16 bit.
    // https://www.binaryhexconverter.com/hex-to-decimal-converter
    function parseHexLittleEndian(text){
        if (text == undefined || !(text.match(/../g)) ){
            return '';
        }
        return parseInt(text.match(/../g).reverse().join(''),16);
    }

    function decodeChatLink3(input) {
        /** Example usage: decodeChatLink('[&AdsnAAA=]')
         *
         * Some examples that can be decoded:
         * '[&AdsnAAA=]'; // Coin - 1g 02s 03c
         * '[&AgGqtgAA]'; // Item - Zojja's Claymore
         * '[&AgGqtgDgfQ4AAP9fAAAnYAAA]'; // Item - Zojja's Claymore (item 46762), bitmask 224 (skin + two upgrades) skinned as Dreamthistle Greatsword (skin 3709), with Superior Bloodlust (item 24575), Superior Force (24615)
         * '[&AxcnAAA=]'; // Text: "Fight what cannot be fought" - id 10007
         * '[&DGYAAABOBAAA]'; // WvW objective: [[Y'lan Academy]] --> map id 1102, objective 102
         * '[&DAYAAAAmAAAA]'; // WvW objective: [[Speldan Clearcut]] --> map id 38, objective 6
         * '[&DQQIByEANzZ5AHgAqwEAALUApQEAALwA7QDtABg9AAEAAAAAAAAAAAAAAAA=]'; // Ranger build
         * '[&DQEqHhAaPj1LFwAAFRcAAEgBAAAxAQAANwEAAAAAAAAAAAAAAAAAAAAAAAA=]'; // Burn firebrand
         */

        // Input cleanup - remove "[&" and rear "]"
        var code = input.replace(/^\[\&+|\]+$/g, '');
        
        // Also remove base64 padding from end (= sign)
        code = code.replace(/=+$/, '');

        // Split characters into array
        var textArray = code.split('');

        // Convert from Text to an Array of decimal numbers (0, 1, 2, 3, 4, 5, 6, 7, 8, 9).
        var AtoB_lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
        var decimals = new Array(textArray.length);
        for (var i = 0; i < code.length; i++) {
            decimals[i] = AtoB_lookup.indexOf(textArray[i]);
        }

        // Convert from numbers to blocks of 6 binary bits [aka digits]
        //  Needs 6 digits 32-16-8-4-2-1 to represent 0-63 for a base 64 number
        var binaries = new Array(code.length);
        for (var i = 0; i < code.length; i++) {
            binaries[i] = decimals[i].toString(2).padStart(6,'0');
        }

        // Join
        var binary_stream = binaries.join('');

        // Split into blocks, but this time in groups of 8 binary bits (1 byte)
        var binary_octets = binary_stream.match(/......../g);

        // Interpret as HEX
        var hex_octets = new Array(binary_octets.length);
        for (var i = 0; i < binary_octets.length; i++) {
            hex_octets[i] = parseInt(binary_octets[i], 2).toString(16).padStart(2,'0').toUpperCase();
        }

        // Join
        var hex_stream = hex_octets.join('');
        
        // Note: Two hex characters together are called a "byte" (because it maps back to 8 binary bits).

        // Layout specifications change depending on the header number
        var specification = {
            1: {
                name: 'coin',
                searchflag: 'n',
                extendedoutput: 'n',
                format: [
                    { name: 'header',     bytes: 1 },
                    { name: 'copper_qty', bytes: 4 }
                ]
            },
            2: {
                name: 'item',
                searchflag: 'y',
                extendedoutput: 'n',
                format: [
                    { name: 'header',    bytes: 1 },
                    { name: 'quantity',  bytes: 1 },
                    { name: 'id',        bytes: 3 },
                    { name: 'bitmask',   bytes: 1 }, // Bitmask meanings: 0 = no upgrades, 64 (or 32) = 1 sigil, 96 = 2 sigils, 128 = skinned, 192 (or 160) = skinned + 1 sigil, 224 = skinned + 2 sigils
                    { name: 'upgrade1',  bytes: 3 },
                    { name: 'padding1',  bytes: 1 },
                    { name: 'upgrade2',  bytes: 3 },
                    { name: 'padding2',  bytes: 1 },
                    { name: 'upgrade3',  bytes: 3 }
                ]
            },
            3: {
                name: 'text',
                searchflag: 'n',
                extendedoutput: 'n',
                format: [
                    { name: 'header',    bytes: 1 },
                    { name: 'id',        bytes: 3 }
                ]
            },
            4: {
                name: 'location',
                searchflag: 'y',
                extendedoutput: 'n',
                format: [
                    { name: 'header',    bytes: 1 },
                    { name: 'id',        bytes: 3 }
                ]
            },
            6: {
                name: 'skill',
                searchflag: 'y',
                extendedoutput: 'n',
                format: [
                    { name: 'header',    bytes: 1 },
                    { name: 'id',        bytes: 3 }
                ]
            },
            7: {
                name: 'trait',
                searchflag: 'y',
                extendedoutput: 'n',
                format: [
                    { name: 'header',    bytes: 1 },
                    { name: 'id',        bytes: 3 }
                ]
            },
            9: {
                name: 'recipe',
                searchflag: 'y',
                extendedoutput: 'n',
                format: [
                    { name: 'header',    bytes: 1 },
                    { name: 'id',        bytes: 3 }
                ]
            },
            10: {
                name: 'skin',
                searchflag: 'y',
                extendedoutput: 'n',
                format: [
                    { name: 'header',    bytes: 1 },
                    { name: 'id',        bytes: 3 }
                ]
            },
            11: {
                name: 'outfit',
                searchflag: 'y',
                extendedoutput: 'n',
                format: [
                    { name: 'header',    bytes: 1 },
                    { name: 'id',        bytes: 3 }
                ]
            },
            12: {
                name: 'wvw objective',
                searchflag: 'y',
                extendedoutput: 'n',
                format: [
                    { name: 'header',    bytes: 1 },
                    { name: 'id',        bytes: 3 },
                    { name: 'padding1',  bytes: 1 },
                    { name: 'map_id',    bytes: 3 },
                    { name: 'padding2',  bytes: 1 }
                ]
            },
            13: {
                name: 'build template',
                searchflag: 'n',
                extendedoutput: 'y',
                format: [
                    { name: 'header',                        bytes: 1 },
                    { name: 'prof',                          bytes: 1 },
                    { name: 'spec1',                         bytes: 1 }, // 1oo6 bytes
                    { name: 'spec1_choices',                 bytes: 1 }, // 2oo6
                    { name: 'spec2',                         bytes: 1 }, // 3oo6
                    { name: 'spec2_choices',                 bytes: 1 }, // 4oo6
                    { name: 'spec3',                         bytes: 1 }, // 5oo6
                    { name: 'spec3_choices',                 bytes: 1 }, // 6oo6
                    { name: 'heal',                          bytes: 2 }, // 2oo20 bytes
                    { name: 'aquatic_heal',                  bytes: 2 }, // 4oo20
                    { name: 'utility1',                      bytes: 2 }, // 6oo20
                    { name: 'aquatic_utility1',              bytes: 2 }, // 8oo20
                    { name: 'utility2',                      bytes: 2 }, // 10oo20
                    { name: 'aquatic_utility2',              bytes: 2 }, // 12oo20
                    { name: 'utility3',                      bytes: 2 }, // 14oo20
                    { name: 'aquatic_utility3',              bytes: 2 }, // 16oo20
                    { name: 'elite',                         bytes: 2 }, // 18oo20
                    { name: 'aquatic_elite',                 bytes: 2 }, // 20oo20
                    { name: 'pet1ORrevlegend1',              bytes: 1 }, // 1oo4 bytes
                    { name: 'pet2ORrevlegend2',              bytes: 1 }, // 2oo4
                    { name: 'aquatic_pet1ORrevlegend1',      bytes: 1 }, // 3oo4
                    { name: 'aquatic_pet2ORrevlegend2',      bytes: 1 }, // 4oo4
                    { name: 'inactive_rev_utility1'   ,      bytes: 2 }, // 2oo6
                    { name: 'inactive_rev_utility2'   ,      bytes: 2 }, // 4oo6
                    { name: 'inactive_rev_utility3'   ,      bytes: 2 }, // 6oo6
                    { name: 'inactive_aquatic_rev_utility1', bytes: 2 }, // 2oo6
                    { name: 'inactive_aquatic_rev_utility2', bytes: 2 }, // 4oo6
                    { name: 'inactive_aquatic_rev_utility3', bytes: 2 }  // 6oo6
                ]
            },
            14: {
                name: 'achievement',
                searchflag: 'y',
                extendedoutput: 'n',
                format: [
                    { name: 'header',    bytes: 1 },
                    { name: 'id',        bytes: 3 }
                ]
            },
            15: {
                name: 'fashion template',
                searchflag: 'n',
                extendedoutput: 'y',
                format: [
                    { name: 'header',             bytes: 1 },
                    { name: 'aquabreather',       bytes: 2 }, // aquabreather
                    { name: 'armor1',             bytes: 2 }, // 2oo10 bytes - back
                    { name: 'armor1dye1',         bytes: 2 }, // 4oo10
                    { name: 'armor1dye2',         bytes: 2 }, // 6oo10
                    { name: 'armor1dye3',         bytes: 2 }, // 8oo10
                    { name: 'armor1dye4',         bytes: 2 }, // 10oo10
                    { name: 'armor2',             bytes: 2 }, // 2oo10 bytes - coat
                    { name: 'armor2dye1',         bytes: 2 }, // 4oo10
                    { name: 'armor2dye2',         bytes: 2 }, // 6oo10
                    { name: 'armor2dye3',         bytes: 2 }, // 8oo10
                    { name: 'armor2dye4',         bytes: 2 }, // 10oo10
                    { name: 'armor3',             bytes: 2 }, // 2oo10 bytes - boots
                    { name: 'armor3dye1',         bytes: 2 }, // 4oo10
                    { name: 'armor3dye2',         bytes: 2 }, // 6oo10
                    { name: 'armor3dye3',         bytes: 2 }, // 8oo10
                    { name: 'armor3dye4',         bytes: 2 }, // 10oo10
                    { name: 'armor4',             bytes: 2 }, // 2oo10 bytes - gloves
                    { name: 'armor4dye1',         bytes: 2 }, // 4oo10
                    { name: 'armor4dye2',         bytes: 2 }, // 6oo10
                    { name: 'armor4dye3',         bytes: 2 }, // 8oo10
                    { name: 'armor4dye4',         bytes: 2 }, // 10oo10
                    { name: 'armor5',             bytes: 2 }, // 2oo10 bytes - helm
                    { name: 'armor5dye1',         bytes: 2 }, // 4oo10
                    { name: 'armor5dye2',         bytes: 2 }, // 6oo10
                    { name: 'armor5dye3',         bytes: 2 }, // 8oo10
                    { name: 'armor5dye4',         bytes: 2 }, // 10oo10
                    { name: 'armor6',             bytes: 2 }, // 2oo10 bytes - legs
                    { name: 'armor6dye1',         bytes: 2 }, // 4oo10
                    { name: 'armor6dye2',         bytes: 2 }, // 6oo10
                    { name: 'armor6dye3',         bytes: 2 }, // 8oo10
                    { name: 'armor6dye4',         bytes: 2 }, // 10oo10
                    { name: 'armor7',             bytes: 2 }, // 2oo10 bytes - shoulders
                    { name: 'armor7dye1',         bytes: 2 }, // 4oo10
                    { name: 'armor7dye2',         bytes: 2 }, // 6oo10
                    { name: 'armor7dye3',         bytes: 2 }, // 8oo10
                    { name: 'armor7dye4',         bytes: 2 }, // 10oo10
                    { name: 'outfit',             bytes: 2 }, // 2oo10 bytes
                    { name: 'outfitdye1',         bytes: 2 }, // 4oo10
                    { name: 'outfitdye2',         bytes: 2 }, // 6oo10
                    { name: 'outfitdye3',         bytes: 2 }, // 8oo10
                    { name: 'outfitdye4',         bytes: 2 }, // 10oo10
                    { name: 'aquaweapon1',        bytes: 2 }, // 2oo4 bytes
                    { name: 'aquaweapon2',        bytes: 2 }, // 4oo4
                    { name: 'weapon1',            bytes: 2 }, // 2oo8 bytes
                    { name: 'weapon2',            bytes: 2 }, // 4oo8
                    { name: 'weapon3',            bytes: 2 }, // 6oo8
                    { name: 'weapon4',            bytes: 2 }, // 8oo8
                    { name: 'hiddenslots',        bytes: 2 }  // Bitmask meanings: Each digit represents, in sequence, each of the above slots being visible (e.g. first is aquabreather, last is weapon4)
                ]
            },
            16: {
                name: 'travel template',
                searchflag: 'n',
                extendedoutput: 'y',
                format: [
                    { name: 'header',              bytes: 1 },
                    
                    { name: 'glider-skin',         bytes: 2 }, // glider
                    { name: 'glider-dye1',         bytes: 2 },
                    { name: 'glider-dye2',         bytes: 2 },
                    { name: 'glider-dye3',         bytes: 2 },
                    { name: 'glider-dye4',         bytes: 2 },
                    
                    { name: 'door-skin',           bytes: 2 }, // conjured doorway
                    { name: 'door-dye1',           bytes: 2 },
                    { name: 'door-dye2',           bytes: 2 },
                    { name: 'door-dye3',           bytes: 2 },
                    { name: 'door-dye4',           bytes: 2 },
                    
                    { name: 'mount-jackal-skin',   bytes: 2 }, // jackal
                    { name: 'mount-jackal-dye1',   bytes: 2 },
                    { name: 'mount-jackal-dye2',   bytes: 2 },
                    { name: 'mount-jackal-dye3',   bytes: 2 },
                    { name: 'mount-jackal-dye4',   bytes: 2 },
                    
                    { name: 'mount-griffon-skin',  bytes: 2 }, // griffon
                    { name: 'mount-griffon-dye1',  bytes: 2 },
                    { name: 'mount-griffon-dye2',  bytes: 2 },
                    { name: 'mount-griffon-dye3',  bytes: 2 },
                    { name: 'mount-griffon-dye4',  bytes: 2 },
                    
                    { name: 'mount-springer-skin', bytes: 2 }, // springer
                    { name: 'mount-springer-dye1', bytes: 2 },
                    { name: 'mount-springer-dye2', bytes: 2 },
                    { name: 'mount-springer-dye3', bytes: 2 },
                    { name: 'mount-springer-dye4', bytes: 2 },
                    
                    { name: 'mount-skimmer-skin',  bytes: 2 }, // skimmer
                    { name: 'mount-skimmer-dye1',  bytes: 2 },
                    { name: 'mount-skimmer-dye2',  bytes: 2 },
                    { name: 'mount-skimmer-dye3',  bytes: 2 },
                    { name: 'mount-skimmer-dye4',  bytes: 2 },
                    
                    { name: 'mount-raptor-skin',   bytes: 2 }, // raptor
                    { name: 'mount-raptor-dye1',   bytes: 2 },
                    { name: 'mount-raptor-dye2',   bytes: 2 },
                    { name: 'mount-raptor-dye3',   bytes: 2 },
                    { name: 'mount-raptor-dye4',   bytes: 2 },
                    
                    { name: 'mount-beetle-skin',   bytes: 2 }, // roller beetle
                    { name: 'mount-beetle-dye1',   bytes: 2 },
                    { name: 'mount-beetle-dye2',   bytes: 2 },
                    { name: 'mount-beetle-dye3',   bytes: 2 },
                    { name: 'mount-beetle-dye4',   bytes: 2 },
                    
                    { name: 'mount-warclaw-skin',  bytes: 2 }, // warclaw
                    { name: 'mount-warclaw-dye1',  bytes: 2 },
                    { name: 'mount-warclaw-dye2',  bytes: 2 },
                    { name: 'mount-warclaw-dye3',  bytes: 2 },
                    { name: 'mount-warclaw-dye4',  bytes: 2 },
                    
                    { name: 'mount-skyscale-skin', bytes: 2 }, // skyscale
                    { name: 'mount-skyscale-dye1', bytes: 2 },
                    { name: 'mount-skyscale-dye2', bytes: 2 },
                    { name: 'mount-skyscale-dye3', bytes: 2 },
                    { name: 'mount-skyscale-dye4', bytes: 2 },
                    
                    { name: 'skiff-skin',          bytes: 2 }, // skiff
                    { name: 'skiff-dye1',          bytes: 2 },
                    { name: 'skiff-dye2',          bytes: 2 },
                    { name: 'skiff-dye3',          bytes: 2 },
                    { name: 'skiff-dye4',          bytes: 2 },
                    
                    { name: 'mount-turtle-skin',   bytes: 2 }, // siege turtle
                    { name: 'mount-turtle-dye1',   bytes: 2 },
                    { name: 'mount-turtle-dye2',   bytes: 2 },
                    { name: 'mount-turtle-dye3',   bytes: 2 },
                    { name: 'mount-turtle-dye4',   bytes: 2 },
                    
                    { name: 'hiddenmask',          bytes: 2 }  // 
                ]
            }
        };

        // Examine the header - this informs the structure of the rest of the chatlink
        var headerTypeNum = parseHexLittleEndian( hex_stream.slice(0,2) );
        if (!(headerTypeNum in specification)){
            // Chatlink header type not supported
            return {
                headername: 'unsupported',
                header: headerTypeNum,
                searchflag: 'n'
            };
        }

        // Convert hex stream into blocks of decimals
        var hex_spec = {}, dec_spec = { 'headername': '' }, offset = 0;
        $.each( specification[headerTypeNum].format, function(i,v){
            hex_spec[v.name] = hex_stream.slice(offset, offset + v.bytes*2);
            dec_spec[v.name] = parseHexLittleEndian( hex_spec[v.name] );
            offset += v.bytes*2;
        });


        // Push the header name and wiki seach true/false flag
        dec_spec.headername = specification[headerTypeNum].name;
        dec_spec.searchflag = specification[headerTypeNum].searchflag;
        dec_spec.extendedoutput = specification[headerTypeNum].extendedoutput;

        // Extra sanitization due to printing the json blob
        if (dec_spec.headername == 'item') {
            // Upgrades
            var i_temp = itemChoices(dec_spec.bitmask);
            
            // Name
            dec_spec.enhancements = i_temp.name;
            
            // Rename remaining variables
            var i_temp_upgrades_array = [dec_spec.upgrade1,dec_spec.upgrade2,dec_spec.upgrade3];
            var i_count = 0;
            $.each(i_temp.arr, function(i,v) {
                if (v !== '') {
                    if (v == 'skin') {
                        dec_spec.skin = i_temp_upgrades_array[i];
                    } else {
                        i_count += 1;
                        dec_spec['item_upgrade_id' + i_count] = i_temp_upgrades_array[i];
                    }
                }
            });
            
            delete dec_spec.bitmask;
            delete dec_spec.upgrade1;
            delete dec_spec.upgrade2;
            delete dec_spec.upgrade3;
            delete dec_spec.padding1;
            delete dec_spec.padding2;
            delete dec_spec.padding3;
        }
        if (dec_spec.headername == 'build template') {
            // Specializations
            dec_spec.spec1_choices = specializationChoices(dec_spec.spec1_choices).join('-');
            dec_spec.spec2_choices = specializationChoices(dec_spec.spec2_choices).join('-');
            dec_spec.spec3_choices = specializationChoices(dec_spec.spec3_choices).join('-');

            // Ranger and Revenant specific
            // Ranger
            if (dec_spec.prof == 4) {
                dec_spec.pet1 = dec_spec.pet1ORrevlegend1;
                dec_spec.pet2 = dec_spec.pet2ORrevlegend2;
                dec_spec.aquatic_pet1 = dec_spec.aquatic_pet1ORrevlegend1;
                dec_spec.aquatic_pet2 = dec_spec.aquatic_pet2ORrevlegend2;
            }

            // Revenant
            if (dec_spec.prof == 9) {
                dec_spec.revlegend1 = dec_spec.pet1ORrevlegend1;
                dec_spec.revlegend2 = dec_spec.pet2ORrevlegend2;
                dec_spec.aquatic_revlegend1 = dec_spec.aquatic_pet1ORrevlegend1;
                dec_spec.aquatic_revlegend2 = dec_spec.aquatic_pet2ORrevlegend2;
            } else {
                delete dec_spec.inactive_rev_utility1;
                delete dec_spec.inactive_rev_utility2;
                delete dec_spec.inactive_rev_utility3;
                delete dec_spec.inactive_aquatic_rev_utility1;
                delete dec_spec.inactive_aquatic_rev_utility2;
                delete dec_spec.inactive_aquatic_rev_utility3;
            }

            delete dec_spec.pet1ORrevlegend1;
            delete dec_spec.pet2ORrevlegend2;
            delete dec_spec.aquatic_pet1ORrevlegend1;
            delete dec_spec.aquatic_pet2ORrevlegend2;
            
            // Check for any remaining hex (potential for build templates)
            if (hex_stream.length - offset > 0) {

                // First byte is a header identifying the length of the equipped (land) weapon information
                var bytes = 1;
                var weapon_count = parseHexLittleEndian( hex_stream.slice(offset, offset + bytes*2) );
                offset += bytes*2;
                
                // If there are weapons, each one is two bytes each
                bytes = 2;
                if (weapon_count > 0) {
                    dec_spec.weapons = [];
                    for (var i = 0; i < weapon_count; i++) {
                        dec_spec.weapons.push( weaponTypeNames(parseHexLittleEndian( hex_stream.slice(offset, offset + bytes*2) )) );
                        offset += bytes*2;
                    }
                    dec_spec.weapons = dec_spec.weapons.join('-');
                }
                
                // Check the next byte
                bytes = 1;
                var override_count = parseHexLittleEndian( hex_stream.slice(offset, offset + bytes*2) );
                offset += bytes*2;
                
                // If there are overrides, each one is four bytes each
                bytes = 4;
                if (override_count > 0) {
                    dec_spec.skill_overrides = [];
                    for (var i = 0; i < override_count; i++) {
                        dec_spec.overrides.push( parseHexLittleEndian( hex_stream.slice(offset, offset + bytes*2) ) );
                        offset += bytes*2;
                    }
                    dec_spec.skill_overrides = dec_spec.skill_overrides.join('-');
                }
            }
        }
        if (dec_spec.headername == 'fashion template') {
            // Masks
            dec_spec.hiddenslots = itemVisibility(dec_spec.hiddenslots).join('-');
        }
        if (dec_spec.headername == 'travel template') {
            // Masks
            dec_spec.hiddenmask = travelVisibility(dec_spec.hiddenmask).join('-');
        }

        return dec_spec;
    }

    function smwAskArticle (data, callback) {
        var apiData = { action: 'ask', query: '?Has canonical name|?Has context|limit=1|' };
        var query = '[[:+]] [[Has game id::' + data.id + ']]';
		switch (data.headername) {
			case 'item':
				query += '[[Has context::Item]]';
			break;
			
			case 'location':
				query += '[[Has context::Location]]';
			break;
			
			case 'skill':
				query = query + '[[Has context::Skill]] OR ' + query + '[[Has context::Effect]]';
			break;
			
			case 'trait':
				query += '[[Has context::Trait]]';
			break;
			
			case 'skin':
				query += '[[Has context::Skin]]';
			break;
			
			case 'recipe':
				query = '[[:+]] [[Has recipe id::' + data.id + ']]';
			break;
			
			case 'outfit':
				query = '[[:+]] [[Has outfit id::' + data.id + ']]';
			break;
			
			case 'wvw objective':
				query = '[[:+]] [[Has wvw objective id::' + data.map_id + '-' + data.id + ']]';
			break;
			
			case 'achievement':
				query += '[[Has context::Achievement]]';
			break;
		}

        apiData.query += query;
        mwApi.get(apiData)
        .done(function (apidata) {
            if (apidata.query.results.length === 0) {
                callback(null);
            }
            else {
                for (var title in apidata.query.results) {
                    var canonicalName = apidata.query.results[title].printouts['Has canonical name'][0];
                    var gameContexts = apidata.query.results[title].printouts['Has context']
                    callback(title, canonicalName, gameContexts.length ? gameContexts[0] : null);
                    return;
                }
            }
        })
        .fail(function (apidata) {
            callback(null);
        });
    }

    function capitalizeFirstLetter(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    }

    function sanitizeForTitleAttr(obj) {
        delete obj.searchflag;

        // Remove empty key-value pairs
        $.each(obj, function(i,v){
            if (v === "") {
                delete obj[i];
            }
        });

        // Replace quote marks, newlines and double spaces
        return JSON.stringify(obj, null, 2)
            .replace(/"/g,"")
            .replace(/\n/g,"")
            .replace(/  /g," ");
    }

    function display (code, listItem) {
        var data = decodeChatLink3(code);
        var type = data.headername;
        var searchflag = data.searchflag;
        var extendedoutput = data.extendedoutput;

        if (searchflag == 'n') {
            if (type == 'unsupported') {
                var span = document.createElement('span');
                span.innerHTML = 'This type of chat link is not recognized and has not been decoded. (Chat link header #' + data.header + ')';
                span.title = sanitizeForTitleAttr(data);
                $(span).fadeIn(1000).appendTo(listItem);
                return;
            } else {
                if (extendedoutput == 'y') {
                    var span = document.createElement('span');
                    span.innerHTML = capitalizeFirstLetter(type) + ' chat link. Searching for this type of chat link is not currently supported, but it has been decoded, see the extended output below'
                    if ('id' in data ) {
                        span.innerHTML += ' (' + type +  ' #' + data.id + ')';
                    }
                    span.title = sanitizeForTitleAttr(data);
                    
                    var div = document.createElement('div');
                    var rid = 'R' + Math.floor(Math.random()*100000);
                    div.id = rid;
                    div.style.border = '1px solid #AAA';
                    $(span).append(div);
                    $(span).fadeIn(1000).appendTo(listItem);
                    
                    // Async conversion of data into wiki output
                    convertWikiMarkupToHTML(data, rid);
                    return;
                } else {
                    var span = document.createElement('span');
                    span.innerHTML = capitalizeFirstLetter(type) + ' chat link. Searching for this type of chat link is not currently supported, but it has been decoded, hover over this line for details.'
                    if ('id' in data ) {
                        span.innerHTML += ' (' + type +  ' #' + data.id + ')';
                    }
                    span.title = sanitizeForTitleAttr(data);
                    $(span).fadeIn(1000).appendTo(listItem);
                    return;
                }
            }

        } else {
            smwAskArticle(data, function (title, canonicalName, gameContext) {
                var span = document.createElement('span');
                span.title = sanitizeForTitleAttr(data);
                if (title) {
                    // If a single chatlink returns a single result (single li element), redirect to that page
                    //  but don't redirect if it contains anything except a chatlink, e.g. interwiki prefix or text following
                    if (searchBar.value.match(/^\[&[A-Za-z0-9+/=]+\]$/)) {
                        // Redirect only once for the current browsing session for that precise result
                        var key = 'searchredirected-' + searchBar.value;
                        try {
                            if (!sessionStorage.getItem(key)) {
                                sessionStorage.setItem(key, 'true');
                                document.location = '/index.php?title=' + encodeURIComponent(title.replace(/ /g, '_'));
                            }
                        } catch(e) {
                            // This might throw if session storage is disabled or unsupported. Just don't redirect if so.
                        }
                    }

                    var link = document.createElement('a');
                    link.href = '/wiki/' + $.map(title.split('/'), function(v){
                        return encodeURIComponent(v.replace(/ /g, '_'));
                    }).join('/');
                    link.title = title;
                    link.innerHTML = canonicalName || title;
                    span.appendChild(link);
                    if (type == 'skill' && gameContext == 'Effect') {
                        type = 'effect';
                    }
                    span.appendChild(document.createTextNode(' (' + type + ' #' + data.id + ')'));
                }
                else {
                    var msg = 'There is no article linked with this ID (' + data.id + ') yet.';
                    msg += ' If you know what <i>' + (type == 'skill' ? 'skill or effect' : type) + '</i> this chat link links to, please add the ID to the article or create it if it does not exist yet.';
                    span.innerHTML = msg;
                }
                $(span).fadeIn(1000).appendTo(listItem);
                $(listItem).attr('data-gameid', data.id)
            });
        }
    }

    window.mw.loader.using('mediawiki.api', function() {
        mwApi = new window.mw.Api();

        // Find chat links
        var ul = document.createElement('ul');
        var expr = /\[&([A-Za-z0-9+/]+=*)\]/g;
        var match;
        while ((match = expr.exec(searchBar.value))) {
            var li = document.createElement('li');
            li.innerHTML = '<tt>' + match[0] + '</tt>';
            ul.appendChild(li);
            display(match[1], li);
        }

        // Display results
        if (ul.children.length) {
            var div = document.createElement('div');
            div.className = 'gw2w-chat-link-search';
            div.innerHTML = 'The following <a href="/wiki/Chat_link_format" title="Chat link format">chat links</a> were included in your search query:';
            div.appendChild(ul);

            var topTable = document.getElementById('mw-search-top-table');
            $(div).hide().insertAfter(topTable).show('fast');
        }
    });
}
/* </nowiki> */