MediaWiki:ChatLinkSearch.js

De Guild Wars 2 Wiki
Ir a la navegaciónIr a la búsqueda

Nota: Después de publicar, quizás necesite actualizar la caché de su navegador para ver los cambios.

  • Firefox/Safari: Mantenga presionada la tecla Shift mientras pulsa el botón Actualizar, o presiona Ctrl+F5 o Ctrl+R (⌘+R en Mac)
  • Google Chrome: presione Ctrl+Shift+R (⌘+Shift+R en Mac)
  • Internet Explorer/Edge: mantenga presionada Ctrl mientras pulsa Actualizar, o presione Ctrl+F5
  • Opera: Presiona 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". (Saelyth note: which is why we didn't translate that one)
 *
 * Original by Patrick Westerhoff [User:Poke]. 2022 modifications by Chieftain Alex.
 */
 
 
/**
 * Entry point, slightly different to EN wiki since does not need to handle interwikis.
 */
 (function chatLinkSearch_wrapper() {
    var searchBar = document.querySelector('#searchText input');
    if (!searchBar) {
        return;
    }

    // Check for an interwiki prefix
    chatLinkSearch(searchBar)
})();

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

function chatLinkSearch(searchBar) {
    var mwApi;

    // 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 = ['objeto','',''];           break;
            case 96:  option.name = 'two sigils/runes';                    option.arr = ['objeto','objeto',''];       break;
            case 128: option.name = 'skin applied';                        option.arr = ['diseño','',''];         break;
            case 160:
            case 192: option.name = 'one sigil/rune and a skin applied';   option.arr = ['diseño','objeto',''];     break;
            case 224: option.name = 'two sigils/runes and a skin applied'; option.arr = ['diseño','objeto','objeto']; break;
            default:  option.name = 'unknown';                             option.arr = ['','',''];               break;
        }
        return option;
    }

    // Helper function: Convert specialization mask into options (top/middle/bottom)
    function specializationChoices(mask){
        // Convert to binary
        var binary = mask.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) - 1;
        });

        var traits = $.map(positions, function(v,k) {
            var pos = ['Top','Middle','Bottom'];
            return pos[v];
        });
        return traits;
    }

    function decodeChatLink2(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
         */

        // HELPER FUNCTION #1
        // 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);
        }

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

        // 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 4 binary bits
        var binary_quads = binary_stream.match(/..../g);

        // Interpret as HEX - one hex character = 4 bits. Therefore two hex characters = 8 bits == 1 byte.
        var hex_quads = new Array(binary_quads.length);
        for (var i = 0; i < binary_quads.length; i++) {
            hex_quads[i] = parseInt(binary_quads[i], 2).toString(16).toUpperCase();
        }

        // Join
        var hex_stream = hex_quads.join('');

        // Layout specifications change depending on the header number
        var specification = {
            1: {
                name: 'coin',
                searchflag: 'n',
                format: [
                    { name: 'header',     bytes: 2 },
                    { name: 'copper_qty', bytes: 8 }
                ]
            },
            2: {
                name: 'objeto', // item
                searchflag: 'y',
                format: [
                    { name: 'header',    bytes: 2 },
                    { name: 'quantity',  bytes: 2 },
                    { name: 'id',        bytes: 6 },
                    { name: 'bitmask',   bytes: 2 }, // 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: 6 },
                    { name: 'padding1',  bytes: 2 },
                    { name: 'upgrade2',  bytes: 6 },
                    { name: 'padding2',  bytes: 2 },
                    { name: 'upgrade3',  bytes: 6 }
                ]
            },
            3: {
                name: 'text',
                searchflag: 'n',
                format: [
                    { name: 'header',    bytes: 2 },
                    { name: 'id',        bytes: 8 }
                ]
            },
            4: {
                name: 'localización', // location
                searchflag: 'y',
                format: [
                    { name: 'header',    bytes: 2 },
                    { name: 'id',        bytes: 8 }
                ]
            },
            6: {
                name: 'habilidad', // skill
                searchflag: 'y',
                format: [
                    { name: 'header',    bytes: 2 },
                    { name: 'id',        bytes: 8 }
                ]
            },
            7: {
                name: 'rasgo', // trait
                searchflag: 'y',
                format: [
                    { name: 'header',    bytes: 2 },
                    { name: 'id',        bytes: 8 }
                ]
            },
            9: {
                name: 'receta', // recipe
                searchflag: 'y',
                format: [
                    { name: 'header',    bytes: 2 },
                    { name: 'id',        bytes: 8 }
                ]
            },
            10: {
                name: 'diseño', // skin
                searchflag: 'y',
                format: [
                    { name: 'header',    bytes: 2 },
                    { name: 'id',        bytes: 8 }
                ]
            },
            11: {
                name: 'atuendo', // outfit
                searchflag: 'y',
                format: [
                    { name: 'header',    bytes: 2 },
                    { name: 'id',        bytes: 8 }
                ]
            },
            12: {
                name: 'wvw objective',
                searchflag: 'n',
                format: [
                    { name: 'header',    bytes: 2 },
                    { name: 'id',        bytes: 8 },
                    { name: 'map_id',    bytes: 8 }
                ]
            },
            13: {
                name: 'build template',
                searchflag: 'n',
                format: [
                    { name: 'header',                    bytes: 2 }, // 2 hex digits (each capable of 0-15 permutations) = 2*4 binary digits = 1 byte
                    { name: 'prof',                      bytes: 2 }, // 1 byte
                    { name: 'spec1',                     bytes: 2 }, // 1oo6 bytes
                    { name: 'spec1_choices',             bytes: 2 }, // 2oo6 bytes
                    { name: 'spec2',                     bytes: 2 }, // 3oo6 bytes
                    { name: 'spec2_choices',             bytes: 2 }, // 4oo6 bytes
                    { name: 'spec3',                     bytes: 2 }, // 5oo6 bytes
                    { name: 'spec3_choices',             bytes: 2 }, // 6oo6 bytes
                    { name: 'heal',                      bytes: 4 }, // 2oo20 bytes
                    { name: 'aquatic_heal',              bytes: 4 }, // 4oo20
                    { name: 'utility1',                  bytes: 4 }, // 6oo20
                    { name: 'aquatic_utility1',          bytes: 4 }, // 8oo20
                    { name: 'utility2',                  bytes: 4 }, // 10oo20
                    { name: 'aquatic_utility2',          bytes: 4 }, // 12oo20
                    { name: 'utility3',                  bytes: 4 }, // 14oo20
                    { name: 'aquatic_utility3',          bytes: 4 }, // 16oo20
                    { name: 'elite',                     bytes: 4 }, // 18oo20
                    { name: 'aquatic_elite',             bytes: 4 }, // 20oo20
                    { name: 'pet1ORrevlegend1',          bytes: 2 }, // 1oo4 bytes
                    { name: 'pet2ORrevlegend2',          bytes: 2 }, // 2oo4
                    { name: 'aquatic_pet1ORarevlegend1', bytes: 2 }, // 3oo4
                    { name: 'aquatic_pet2ORarevlegend2', bytes: 2 }  // 4oo4
                ]
            }
        };

        // 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);
            dec_spec[v.name] = parseHexLittleEndian( hex_spec[v.name] );
            offset += v.bytes;
        });


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

        // Extra sanitization due to printing the json blob
        if (dec_spec.headername == 'objeto') {
            // 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_pet1ORarevlegend1;
                dec_spec.aquatic_pet2 = dec_spec.aquatic_pet2ORarevlegend2;
            }

            // 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_pet1ORarevlegend1;
                dec_spec.aquatic_revlegend2 = dec_spec.aquatic_pet2ORarevlegend2;
            }

            delete dec_spec.pet1ORrevlegend1;
            delete dec_spec.pet2ORrevlegend2;
            delete dec_spec.aquatic_pet1ORarevlegend1;
            delete dec_spec.aquatic_pet2ORarevlegend2;
        }

        return dec_spec;
    }

    function smwAskArticle (type, id, callback) {
        var apiData = { action: 'ask', query: '?Tiene canónico nombre|?Tiene contexto|limit=1|' };
        var query = '[[:+]] [[Has game id::' + id + ']]';
        if (type == 'objeto') {
            query += '[[Tiene contexto::Objeto]]';
        }
        else if (type == 'localización') {
            query += '[[Tiene contexto::Localización]]';
        }
        else if (type == 'habilidad') {
            query = query + '[[Tiene contexto::Habilidad]] OR ' + query + '[[Tiene contexto::Efecto]]';
        }
        else if (type == 'rasgo') {
            query += '[[Tiene contexto::Rasgo]]';
        }
        else if (type == 'diseño') {
            query += '[[Tiene contexto::Diseño]]';
        }
        else if (type == 'receta') {
            query = '[[:+]] [[Tiene id de receta::' + id + ']]';
        }
        else if (type == 'atuendo') {
            query += '[[:+]] [[Tiene id de atuendo::' + id + ']]';
        }
        apiData.query += query;
        mwApi.get(apiData)
        .done(function (data) {
            if (data.query.results.length === 0) {
                callback(null);
            }
            else {
                for (var title in data.query.results) {
                    var canonicalName = data.query.results[title].printouts['Tiene canónico nombre'][0];
                    var gameContexts = data.query.results[title].printouts['Tiene contexto']
                    callback(title, canonicalName, gameContexts.length ? gameContexts[0] : null);
                    return;
                }
            }
        })
        .fail(function (data) {
            callback(null);
        });
    }

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

    function sanitizeTitle(obj) {
        delete obj.searchflag;

        // Remove emtpy 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 = decodeChatLink2(code);
        var type = data.headername;
        var id;
        var searchflag = data.searchflag;

        if (searchflag == 'n') {
            if (type == 'unsupported') {
                var span = document.createElement('span');
                span.innerHTML = 'Este tipo de enlace de chat no está soportado de momento (Chat link header #' + data.header + ')';
                span.title = sanitizeTitle(data);
                $(span).fadeIn(1000).appendTo(listItem);
                return;
            } else {
                var span = document.createElement('span');
                span.innerHTML = capitalizeFirstLetter(type) + ' enlace de chat. Actualmente no se admite la búsqueda de este tipo de enlace de chat, pero se ha decodificado. Pase el cursor sobre esta línea para obtener más detalles.'
                if ('id' in data ) {
                    span.innerHTML += ' (' + type +  ' #' + data.id + ')';
                }
                span.title = sanitizeTitle(data);
                $(span).fadeIn(1000).appendTo(listItem);
                return;
            }

        } else {
            id = data.id;

            smwAskArticle(type, id, function (title, canonicalName, gameContext) {
                var span = document.createElement('span');
                span.title = sanitizeTitle(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 == 'habilidad' && gameContext == 'Efecto') {
                        type = 'efecto';
                    }
                    span.appendChild(document.createTextNode(' (' + type + ' #' + id + ')'));
                }
                else {
                    var msg = 'No hay ningún articulo asociado con esta (' + id + ') todavía.';
                    msg += ' Si sabes a qué <i>' + (type == 'habilidad' ? 'habilidad o efecto' : type) + '</i> este código enlaza, por favor añade la ID al articulo o crealo si todavía no existe.';
                    span.innerHTML = msg;
                }
                $(span).fadeIn(1000).appendTo(listItem);
                $(listItem).attr('data-gameid', 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 = 'Los siguiente <a href="/wiki/Chat_link" title="Enlace de chat">enlaces de chat</a> fueron incluidos en tu consulta de busqueda:';
            div.appendChild(ul);

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