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; // 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 (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: '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: 'location', searchflag: 'y', format: [ { name: 'header', bytes: 2 }, { name: 'id', bytes: 8 } ] }, 6: { name: 'skill', searchflag: 'y', format: [ { name: 'header', bytes: 2 }, { name: 'id', bytes: 8 } ] }, 7: { name: 'trait', searchflag: 'y', format: [ { name: 'header', bytes: 2 }, { name: 'id', bytes: 8 } ] }, 9: { name: 'recipe', searchflag: 'y', format: [ { name: 'header', bytes: 2 }, { name: 'id', bytes: 8 } ] }, 10: { name: 'skin', searchflag: 'y', format: [ { name: 'header', bytes: 2 }, { name: 'id', bytes: 8 } ] }, 11: { name: '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 == '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_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: '?Has canonical name|?Has context|limit=1|' }; var query = '[[:+]] [[Has game id::' + id + ']]'; if (type == 'item') { query += '[[Has context::Item]]'; } else if (type == 'location') { query += '[[Has context::Location]]'; } else if (type == 'skill') { query = query + '[[Has context::Skill]] OR ' + query + '[[Has context::Effect]]'; } else if (type == 'trait') { query += '[[Has context::Trait]]'; } else if (type == 'skin') { query += '[[Has context::Skin]]'; } else if (type == 'recipe') { query = '[[:+]] [[Has recipe id::' + id + ']]'; } else if (type == 'outfit') { query = '[[:+]] [[Has outfit id::' + 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['Has canonical name'][0]; var gameContexts = data.query.results[title].printouts['Has context'] 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 = 'This type of chat link is not recognized and has not been decoded. (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) + ' 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 = 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 == 'skill' && gameContext == 'Effect') { type = 'effect'; } span.appendChild(document.createTextNode(' (' + type + ' #' + id + ')')); } else { var msg = 'There is no article linked with this ID (' + 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', 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> */