User:Jack who built the house/getUrlFromInterwikiLink.js

// jshint esnext: false // jshint esversion: 11 // jshint elision: true ( => {

const initPromise = mw.loader.using(['mediawiki.ForeignApi', 'mediawiki.Title', 'mediawiki.util']).then( => { /*    This is the interwiki data for domains that can access each other using cross-site AJAX requests    (all belonging to WMF). Some domains, e.g. wmflabs.org, belong to WMF, but can not access other    domains using AJAX requests. To update this data, run    https://en.wikipedia.org/wiki/User:Jack_who_built_the_house/getInterwikiData.js in the browser    console and copypaste the resulting value.  */  const IW_DATA = {"matchingLangPrefixes":["aa","ab","ace","ady","af","ak","als","alt","am","ami","an","ang","anp","ar","arc","ary","arz","as","ast","atj","av","avk","awa","ay","az","azb","ba","ban","bar","bat-smg","bbc","bcl","be","be-tarask","bg","bh","bi","bjn","blk","bm","bn","bo","bpy","br","bs","btm","bug","bxr","ca","cbk-zam","cdo","ce","ceb","ch","cho","chr","chy","ckb","co","cr","crh","cs","csb","cu","cv","cy","da","dag","de","dga","din","diq","dsb","dty","dv","dz","ee","el","eml","en","eo","es","et","eu","ext","fa","fat","ff","fi","fiu-vro","fj","fo","fon","fr","frp","frr","fur","fy","ga","gag","gan","gcr","gd","gl","glk","gn","gom","gor","got","gpe","gu","guc","gur","guw","gv","ha","hak","haw","he","hi","hif","ho","hr","hsb","ht","hu","hy","hyw","hz","ia","id","ie","ig","ii","ik","ilo","inh","io","is","it","iu","ja","jam","jbo","jv","ka","kaa","kab","kbd","kbp","kcg","kg","ki","kj","kk","kl","km","kn","ko","koi","kr","krc","ks","ksh","ku","kv","kw","ky","la","lad","lb","lbe","lez","lfn","lg","li","lij","lld","lmo","ln","lo","lrc","lt","ltg","lv","mad","mai","map-bms","mdf","mg","mh","mhr","mi","min","mk","ml","mn","mni","mnw","mo","mr","mrj","ms","mt","mus","mwl","my","myv","mzn","na","nah","nap","nds","nds-nl","ne","new","ng","nia","nl","nn","no","nov","nqo","nrm","nso","nv","ny","oc","olo","om","or","os","pa","pag","pam","pap","pcd","pcm","pdc","pfl","pi","pih","pl","pms","pnb","pnt","ps","pt","pwn","qu","rm","rmy","rn","ro","roa-rup","roa-tara","ru","rue","rw","sa","sah","sat","sc","scn","sco","sd","se","sg","sh","shi","shn","shy","si","simple","sk","skr","sl","sm","smn","sn","so","sq","sr","srn","ss","st","stq","su","sv","sw","szl","szy","ta","tay","tcy","te","tet","tg","th","ti","tk","tl","tly","tn","to","tpi","tr","trv","ts","tt","tum","tw","ty","tyv","udm","ug","uk","ur","uz","ve","vec","vep","vi","vls","vo","wa","war","wo","wuu","xal","xh","xmf","yi","yo","yue","za","zea","zgh","zh","zh-classical","zh-min-nan","zu"],"matchingChapterPrefixes":["am","bd","be","br","ca","cn","co","dk","ee","ec","fi","ge","hi","id","mk","mx","nl","nyc","no","pa-us","pl","pt","punjabi","romd","rs","ru","se","tr","ua"],"urlToPrefixes":{"https://advisory.wikimedia.org/wiki/$1":["advisory"],"https://commons.wikimedia.org/wiki/$1":["commons","c"],"https://donate.wikimedia.org/wiki/$1":["donate"],"https://foundation.wikimedia.org/wiki/$1":["foundation","wikimedia","wmf"],"https://incubator.wikimedia.org/wiki/$1":["incubator"],"https://www.wikidata.org/w/index.php?search=$1&ns146=1":["lexemes"],"https://www.mediawiki.org/wiki/$1":["mw","mediawikiwiki"],"https://meta.wikimedia.org/wiki/$1":["meta","metawiki","metawikimedia","metawikipedia","m"],"https://nostalgia.wikipedia.org/wiki/$1":["nost","nostalgia"],"https://wikisource.org/wiki/$1":["oldwikisource"],"https://vrt-wiki.wikimedia.org/wiki/$1":["vrtwiki","otrswiki"],"https://outreach.wikimedia.org/wiki/$1":["outreach","outreachwiki"],"https://www.mediawiki.org/wiki/Special:Code/pywikipedia/$1":["pyrev"],"https://quality.wikimedia.org/wiki/$1":["quality"],"https://www.mediawiki.org/wiki/Special:Code/MediaWiki/$1":["rev"],"https://spcom.wikimedia.org/wiki/$1":["spcom"],"https://species.wikimedia.org/wiki/$1":["species","wikispecies"],"https://strategy.wikimedia.org/wiki/$1":["strategy"],"https://meta.wikimedia.org/wiki/Special:CentralAuth/$1":["sulutil"],"https://ten.wikipedia.org/wiki/$1":["tenwiki"],"https://test.wikipedia.org/wiki/$1":["testwiki"],"https://test.wikidata.org/wiki/$1":["testwikidata"],"https://test2.wikipedia.org/wiki/$1":["test2wiki"],"https://www.mediawiki.org/wiki/Toolserver:$1":["tswiki"],"https://usability.wikimedia.org/wiki/$1":["usability"],"https://wg-en.wikipedia.org/wiki/$1":["wg"],"https://en.wikibooks.org/wiki/$1":["wikibooks"],"https://www.wikidata.org/wiki/$1":["d","wikidata"],"https://www.wikifunctions.org/wiki/$1":["f","wikifunctions"],"https://en.wikinews.org/wiki/$1":["wikinews"],"https://en.wikipedia.org/wiki/$1":["wikipedia"],"https://en.wikipedia.org/wiki/Wikipedia:$1":["wikipediawikipedia"],"https://en.wikiquote.org/wiki/$1":["wikiquote"],"https://en.wikisource.org/wiki/$1":["wikisource"],"https://en.wikiversity.org/wiki/$1":["wikiversity"],"https://en.wikivoyage.org/wiki/$1":["wikivoyage"],"https://beta.wikiversity.org/wiki/$1":["betawikiversity"],"https://en.wiktionary.org/wiki/$1":["wiktionary"],"https://ee.wikimedia.org/wiki/$1":["wmet"],"https://wikimania2005.wikimedia.org/wiki/$1":["wm2005"],"https://wikimania2006.wikimedia.org/wiki/$1":["wm2006"],"https://wikimania2007.wikimedia.org/wiki/$1":["wm2007"],"https://wikimania2008.wikimedia.org/wiki/$1":["wm2008"],"https://wikimania2009.wikimedia.org/wiki/$1":["wm2009"],"https://wikimania2010.wikimedia.org/wiki/$1":["wm2010"],"https://wikimania2011.wikimedia.org/wiki/$1":["wm2011"],"https://wikimania2012.wikimedia.org/wiki/$1":["wm2012"],"https://wikimania2013.wikimedia.org/wiki/$1":["wm2013"],"https://wikimania2014.wikimedia.org/wiki/$1":["wm2014"],"https://wikimania2015.wikimedia.org/wiki/$1":["wm2015"],"https://wikimania2016.wikimedia.org/wiki/$1":["wm2016"],"https://wikimania2017.wikimedia.org/wiki/$1":["wm2017"],"https://wikimania2018.wikimedia.org/wiki/$1":["wm2018"],"https://wikimania.wikimedia.org/wiki/$1":["wmania","wikimania"],"https://wikimaniateam.wikimedia.org/wiki/$1":["wmteam"],"https://be-tarask.$3/wiki/$1":["be-x-old"],"https://als.$3/wiki/$1":["gsw"],"https://zh-classical.$3/wiki/$1":["lzh"],"https://zh-min-nan.$3/wiki/$1":["nan"],"https://roa-rup.$3/wiki/$1":["rup"],"https://bat-smg.$3/wiki/$1":["sgs"],"https://fiu-vro.$3/wiki/$1":["vro"],"https://yue.$3/wiki/$1":["zh-yue"],"https://cs.$3/wiki/$1":["cz"],"https://da.$3/wiki/$1":["dk"],"https://eo.$3/wiki/$1":["epo"],"https://ja.$3/wiki/$1":["jp"],"https://zh.$3/wiki/$1":["zh-cn","zh-tw","cmn"],"https://eml.$3/wiki/$1":["egl"],"https://simple.$3/wiki/$1":["en-simple"],"https://no.$3/wiki/$1":["nb"],"https://$2.wikipedia.org/wiki/$1":["w"],"https://$2.wikiquote.org/wiki/$1":["q"],"https://$2.wikibooks.org/wiki/$1":["b"],"https://$2.wikinews.org/wiki/$1":["n"],"https://$2.wikisource.org/wiki/$1":["s"],"https://$2.wikiversity.org/wiki/$1":["v"],"https://$2.wikivoyage.org/wiki/$1":["voy"],"https://$2.wiktionary.org/wiki/$1":["wikt"],"https://$2.wikimedia.org/wiki/$1":["chapter"],"https://$2.$3/wiki/$1":["_default"]}};

const PREFIX_TO_LANGUAGED_PROJECT = { b: 'wikibooks.org', chapter: 'wikimedia.org', n: 'wikinews.org', q: 'wikiquote.org', s: 'wikisource.org', v: 'wikiversity.org', voy: 'wikivoyage.org', w: 'wikipedia.org', wikt: 'wiktionary.org', };

// The script should work fine even with `IW_DATA = {}`; it would resolve just one interwiki in // that case. IW_DATA.urlToPrefixes = IW_DATA.urlToPrefixes || {}; IW_DATA.matchingLangPrefixes = IW_DATA.matchingLangPrefixes || []; IW_DATA.matchingChapterPrefixes = IW_DATA.matchingChapterPrefixes || [];

const languagedProjectToPrefix = Object.entries(PREFIX_TO_LANGUAGED_PROJECT) // Don't include `chapter`, because many wikimedia.org projects are not languaged .filter(([key]) => key !== 'chapter')

.reduce((obj, [key, value]) => {     obj[value] = key;      return obj;    }, {}); const languagedProjectsPattern = Object.keys(languagedProjectToPrefix).join('|');

const languagedProjectsRegexp = new RegExp(`^(?:([a-z0-9_\\-]+)\\.)?(${languagedProjectsPattern})`);

const langsPattern = IW_DATA.matchingLangPrefixes.join('|'); const languagedProjectsWithMatchingPrefixRegexp = new RegExp(   `^(?:(${langsPattern})\\.)(${languagedProjectsPattern})$`  );

// Here, we utilize the fact that *all* chapter domains are listed under // `IW_DATA.matchingChapterPrefixes`; there are no exceptions where for a chapter domain there is no // `wm%domain%` prefix (unless I was looking in the wrong direction). const chaptersPattern = IW_DATA.matchingChapterPrefixes.join('|'); const chaptersRegexp = new RegExp(`^(${chaptersPattern})\\.(wikimedia\\.org)`);

const prefixToUrl = Object.entries(IW_DATA.urlToPrefixes).reduce((obj, [url, prefixes]) => {   prefixes.forEach((prefix) => { obj[prefix] = url; });   return obj;  }, {});

const iwMap = {};

function getApiUrl(hostname) { return `https://${hostname}/w/api.php`; }

window.getUrlFromInterwikiLink = async function (iwLink, currentHostname = location.hostname) { if (!mw.Title.newFromText(iwLink)) { return null; }

iwLink = iwLink.trim; const match = iwLink.match(/^([a-z0-9_\-:]+):(.*)/i); if (!match) { return mw.util.getUrl(iwLink); }   let [, fullPrefix, pageName] = match;

const hashPos = pageName.indexOf('#'); let fragment = null; if (hashPos !== -1) { fragment = pageName.slice(hashPos + 1); pageName = pageName.slice(0, hashPos); }

let url; const prefixes = fullPrefix.split(':').filter((prefix, i) => prefix || i !== 0); let noMorePrefixes = false; for (let i = 0; i < prefixes.length && !noMorePrefixes; i++) { const prefix = prefixes[i].toLowerCase;

// Two empty prefixes lead to `null` as a return value only if goes after an uninterrupted // series of interwiki prefixes (i.e. no prefix is a namespace prefix). FIXME: well, // `Talk::Something` is wrong too, but `Namespace::Something` is okay. if (prefix ===  && prefixes[i + 1] === ) { return null; }     const urlHostname = url ? (new URL(url)).hostname : currentHostname; const [, currentLanguage, currentLanguagedProject] = (       urlHostname.match(languagedProjectsRegexp)        || urlHostname.match(chaptersRegexp)        || []      ); let newUrl; const chapterPrefix = prefix.slice(2); let skipIwMap = false; if (       ( location.hostname === urlHostname // For the current domain, if the prefix coincides with a namespace, it is treated as a           // namespace ? Object.keys(mw.config.get('wgNamespaceIds')).includes(prefix)

// For other domains, treat prefixes not in lowercase as namespaces : prefix !== prefixes[i] )

// Prefixes matching the current project's prefix itself (i.e. `wikipedia:` on       // en.wikipedia.org, `commons:` on commons.wikimedia.org) are considered namespaces || prefixToUrl[prefix]?.includes(urlHostname)

// There is no `chapter` prefix on chapters themselves (but there is on non-chapter       // wikimedia.org domains). || (         prefix === 'chapter'          && currentLanguagedProject === 'wikimedia.org'          && IW_DATA.matchingChapterPrefixes.includes(currentLanguage)        ) ) {       skipIwMap = true;      } else if (IW_DATA.matchingLangPrefixes.includes(prefix)) {        newUrl = prefixToUrl._default.replace(/\$2/, prefix);      } else if (prefix.match(/^wm.+/) && IW_DATA.matchingChapterPrefixes.includes(chapterPrefix)) {        newUrl = prefixToUrl.chapter.replace(/\$2/, chapterPrefix);      } else {        newUrl = prefixToUrl[prefix];      }

if (!newUrl && !skipIwMap) { try { const apiUrl = getApiUrl(urlHostname); iwMap[apiUrl] = iwMap[apiUrl] || (await new mw.ForeignApi(apiUrl).get({ meta: 'siteinfo', siprop: 'interwikimap', formatversion: 2, })).query.interwikimap; newUrl = iwMap[apiUrl].find(({ prefix: iwPrefix }) => iwPrefix === prefix)?.url; noMorePrefixes = true; } catch { // Empty }     }

if (newUrl) { const unprocessedPrefixes = prefixes .slice(i + 1) .map((prefix, j) => j === 0 && prefix ===  ?  : `${prefix}:`) .join(''); const fragmentEncoded = fragment ? mw.util.escapeIdForLink(fragment) : undefined; newUrl = newUrl // `w:` on `de.wikipedia.org` means `en.wikipedia.org`, and similarly for other project // prefixes. `w:` on `www.wikidata.org` means `en.wikipedia.org` also. .replace(           /\$2/,            currentLanguagedProject === PREFIX_TO_LANGUAGED_PROJECT[prefix]              ? 'en'              : currentLanguage || 'en'          ) // `fr:` on `www.wikidata.org` will mean `fr.wikipedia.org` .replace(/\$3/, currentLanguagedProject || 'wikipedia.org') // Do it last because the page name may contain `$` .replace(         /\$1/,          ( unprocessedPrefixes + mw.util.wikiUrlencode(pageName) + (fragmentEncoded ? `#${fragmentEncoded}` : '') ).replace(/\$/g, '$$$$')       ); } else { newUrl = url || mw.util.getUrl(iwLink); noMorePrefixes = true; }

url = newUrl; }

return url; };

window.getInterwikiPrefixForHostnameSync = function (   targetHostname,    currentHostname = location.hostname  ) { if (targetHostname === currentHostname) { return ''; }

const [, currentLanguage, currentLanguagedProject] = (     currentHostname.match(languagedProjectsWithMatchingPrefixRegexp)

// If we are not on a languaged project with a matching prefix, prefixes behave we same as if     // we are on en.wikipedia.org, except for en.wikipedia.org itself, which is accounted for // below (`parts.push('w');`). Exceptions theoretically could exist, but I didn't find any. // Languaged projects all have matching prefixes for languages, even if there are unmatching // prefixes as well. I.e. there is `cz:` for cs.wikipedia.org, but `cs:` also exists. || [, 'en', 'wikipedia.org'] );   const [, targetLanguage, targetLanguagedProject] = ( targetHostname.match(languagedProjectsWithMatchingPrefixRegexp) || []   );

const parts = []; if (targetLanguagedProject) { if (currentLanguagedProject !== targetLanguagedProject) { parts.push(languagedProjectToPrefix[targetLanguagedProject]); }     if (currentLanguage !== targetLanguage) { parts.push(targetLanguage); }     if (targetHostname === 'en.wikipedia.org' && !parts.length) { parts.push('w'); }   } else { if (targetHostname.endsWith('.wikimedia.org')) { IW_DATA.matchingChapterPrefixes.some((prefix) => {         if (targetHostname.match(new RegExp(`^${prefix}\\.wikimedia\\.org$`))) {            parts.push(currentHostname.match(chaptersRegexp) ? prefix : 'wm' + prefix);         }          return parts.length;        }); }     if (!parts.length) { Object.entries(IW_DATA.urlToPrefixes) // Don't get distracted by additional prefixes for some domains, like `lexemes:` for // https://www.wikidata.org/w/index.php?search=$1&ns146=1. .filter(([iwUrl]) => iwUrl.includes('/wiki/$1')) .some(([iwUrl, prefixes]) => {           if ( targetHostname.match(               new RegExp( '^'                 + mw.util.escapeRegExp((new URL(iwUrl)).hostname) .replace('\\$2', currentLanguage) .replace('\\$3', mw.util.escapeRegExp(currentLanguagedProject)) )             )            ) {              parts.push(prefixes[0]);            }            return parts.length;          }); }   }

return parts.map((part) => part + ':').join('') || null; }; window.getInterwikiPrefixForHostname = async function(...args) { return getInterwikiPrefixForHostnameSync(...args); }; mw.hook('userjs.getUrlFromInterwikiLink.ready').fire({    getUrlFromInterwikiLink,    getInterwikiPrefixForHostname,    getInterwikiPrefixForHostnameSync,  }); });

// Assign a placeholder function that will wait until the modules are loaded and delegate the calls // to it, if any were made. window.getUrlFromInterwikiLink = async (...args) => { await initPromise; return window.getUrlFromInterwikiLink(...args); };

window.getInterwikiPrefixForHostname = async (...args) => { await initPromise; return window.getInterwikiPrefixForHostname(...args); };

});