MediaWiki:Gadget-urldecoder.js: различия между версиями

Материал из Буквицы
Перейти к навигации Перейти к поиску
(+wikidata)
 
мНет описания правки
 
(не показано 8 промежуточных версий этого же участника)
Строка 1: Строка 1:
//[[ВП:Гаджеты/Упрощение ссылок]]
/**
* [[ВП:Гаджеты/Упрощение ссылок]]
*/


if( wgAction == 'edit' || wgAction == 'submit' ){
mw.loader.using( 'ext.gadget.registerTool' ).done( function () {
  mwCustomEditButtons['urldecoder'] = [urlDecoderRun, 'commons/9/91/Link_go.png', 'Раскодировать URL перед курсором или все URL в выделенном тексте']
window.urlDecoderRun = function ( input ) { // main function
  if( ! $.fn.textSelection ) mw.loader.load([ 'jquery.textSelection' ])
}


function urlDecoderRun(){ //main function
// 2nd-lvl WMF domains; old secure link: .../wikipedia/mediawiki,
// .../wikipedia/foundation
var wmDomain = {
mediawiki: 'mw',
wikidata: 'd',
wikisource: 's:mul',
wikimediafoundation: 'foundation'
};


/*
* 2nd-lvl WMF domains with multiple languages; old secure link:
* wikinews/en
*/
var wmDomainM = {
wikipedia: 'w',
wikibooks: 'b',
wikinews: 'n',
wikiquote: 'q',
wikisource: 's',
wikiversity: 'v',
wikivoyage: 'voy',
wiktionary: 'wikt'
};


//2nd-lvl WMF domains; old secure link:    .../wikipedia/mediawiki, .../wikipedia/foundation
/*
var wmDomain  = {
* 3rd-lvl WMF domains on .wikimedia.org; for some reason old secure
mediawiki: 'mw',
* link is wikipedia
wikidata:'d',
*/
wikimediafoundation: 'foundation' }
var wmSubDomains = /^(meta|commons|incubator|species|strategy|wikitech)$/;
//2nd-lvl WMF domains with multiple languages; old secure link:  wikinews/en
var wmDomainM = {
wikipedia:'w',
wikibooks:'b',
wikinews:'n',
wikiquote:'q',
wikisource:'s',
wikiversity:'v',
wikivoyage:'voy',
wiktionary:'wikt'}
//3rd-lvl WMF domains on .wikimedia.org; for some reason old secure link is wikipedia/*
var wmSubDomains = /^(meta|commons|incubator|species|strategy)$/


var httpRegExp = '(https?:\\/\\/[^\\]\\[\\n\\r<>" ]+)' //  any chars except []<>" and \n and spaces
var newText, oldText, rx, isBeforeCursor = false, colonNS, selectionRange,
var localPrefix = WMPrefixes( unSecure(wgServer.replace(/^\/\//,'http://') + wgScript) )
// any chars except []<>" and \n and spaces
var oldText, newText, isBeforeCursor, colonNS
httpRegExp = '(https?:\\/\\/[^\\]\\[\\n\\r<>" ]+)',
localPrefix = WMPrefixes( unSecure( mw.config.get( 'wgServer' ).replace( /^\/\//, 'http://' ) + mw.config.get( 'wgScript' ) ) ),
tbox = $( input || '#wpTextbox1' ).focus(),
caretPos = tbox.textSelection( 'getCaretPosition', {
startAndEnd: true
} ),
selectionStartPos = caretPos[0],
selectionEndPos = caretPos[1];
if (selectionStartPos !== selectionEndPos) { // there was selection
oldText = tbox.textSelection( 'getSelection' );
rx = RegExp( '(\\[{0,2})' + httpRegExp + '([^\\]\\[\\n\\r]*?\\]\\]?)?', 'ig' );
newText = oldText.replace( rx, simplifyMatched );


tbox = $('#wpTextbox1').focus()
if ( window.urlDecoderIntLinks ) {
oldText = tbox.textSelection( 'getSelection' )
var ut = '(' + mw.config.get( 'wgFormattedNamespaces' )[3].replace( / /g, '_' ) + '|user_talk)'; // both
/* localized and canonical 'user_talk' */
ut = RegExp( '\\[\\[' + ut.toLowerCase() + ':[^#]+$', 'i' );
newText = newText.replace( /\[\[[^\]\|\n]+/g, function( lnk ) {
return ut.test( lnk ) ? lnk : decodeAnchor( lnk ); // skip
// user_talk,
// usually
// found in
// signatures
} );
}
if ( newText == oldText ) {
return;
}
} else { // process all text
var content = tbox.textSelection( 'getContents');
isBeforeCursor = true;


// search for http string containing cursor
rx = new RegExp( '(\\[{0,2})' + httpRegExp + '([^\\]\\[\\n\\r]*?\\]\\]?)?', 'ig' );
while ((ma = rx.exec( content )) !== null) {
if ((ma.index <= selectionStartPos) && (selectionStartPos <= (ma.index + ma[0].length))) {
break;
}
}
// (whole
// string)'
// '[',
// 'http:...',
// '
// name]'
if ( !ma ) {
return;
}
oldText = ma[0];
if ( ma[3] ) { // link with name: automatically add brackets
newText = simplifyMatched( ma[0], '[', ma[2], ma[3] + ']' );
} else { // just url: add closing bracket only if there is leading bracket
newText = simplifyMatched( ma[0], ma[1], ma[2], ma[1] ? ']' : '' );
}


if( oldText ){ //there was selection
if ( oldText == newText ) {
return;
}
selectionStartPos = ma.index;
selectionEndPos = selectionStartPos + oldText.length;
}
tbox.textSelection( 'setSelection', {
start: selectionStartPos,
end: selectionEndPos
} );
// replace text
tbox.textSelection( 'encapsulateSelection', {
replace: true,
peri: newText
} );
// FIXME: restructure
if ( selectionRange ) {
tbox.textSelection( 'setSelection', {
start: selectionEndPos - oldText.length + selectionRange[0],
end: selectionEndPos - oldText.length + selectionRange[1]
} );
}


var rx = RegExp('(\\[{0,2})' + httpRegExp + '([^\\]\\[\\n\\r]*?\\]\\]?)?', 'ig')
// end of main code
newText = oldText.replace(rx, simplifyMatched)
return;


if( window.urlDecoderIntLinks ){
// ---FUNCTIONS
  var ut = '(' + wgFormattedNamespaces[3].replace(/ /g,'_') + '|user_talk)' //both localized and canonical 'user_talk'
  ut = RegExp ('\\[\\[' + ut.toLowerCase() + ':[^#]+$', 'i')
  newText = newText.replace(/\[\[[^\]\|\n]+/g, function(lnk){
      return ut.test(lnk) ? lnk : decodeAnchor(lnk) // skip user_talk, usually found in signatures
  })
}


if( newText == oldText) return
function simplifyMatched( str, bracket, url, rest ) { // arguments:
// (whole
// string), '[',
// url, ' name]'
if ( !bracket ) {// no brackets, just url
var trail = RegExp( '[' + ',;\\\\\.:!\\?' + // trailing
// punctuation,
// per Parser.php
( /\(/.test( url ) ? '' : '\\)' ) + // also closing bracket
// without
// opening bracket
']+$' + "|''+$" // or possible bold/italic at the end of url
).exec( url );
if ( trail ) {
url = url.substring( 0, url.length - trail[0].length ); // move
// these
// out
// of
// url
}
return decodeUrl( url ) + str.substring( url.length );


} else if ( rest ) { // both brackets and possibly name
return decodeUrl( url, rest.replace( /\]+$|^ +| +$/g, '' ) ); // trim
// ending
// brackets
// and
// spaces
// in
// 'name]'
} else {
return str; // probably broken wikicode in selected text
}
}


}else{ //process text before cursor
function decodeUrl( url, name ) { // url -> %-decoded -> [[link|name]]
// (if
// possible); name is optional


isBeforeCursor = true
var decodingFailed; // need to skip some strange percent-encoded
// URIs
url = unSecure( url );


//move back enough characters
// percent-decoding
var caretPos = tbox.textSelection('getCaretPosition')
if ( url.indexOf( '%' ) !== -1 ) {
var beginPos = caretPos - 2000
try {
if( beginPos < 0 ) beginPos = 0
url = decodeURI( url );
tbox.textSelection( 'setSelection', {start:beginPos, end:caretPos} )
url = url.replace( /%(3B|2F|2C|3A)/g, decodeURIComponent ); // decode
oldText = tbox.textSelection( 'getSelection' )
// ;/,:
tbox.textSelection( 'setSelection', {start:caretPos, end:caretPos} )
url = url.replace( /[ <>"\[\]\n\r]/g, encodeURIComponent ); // "
/*
* some disallowed chars can screw template params
*/
} catch ( e ) {
decodingFailed = true;
}
}


//try to find http in oldText
if ( isBeforeCursor ) { // user-defined conversion to eng keywords
var rx = new RegExp('(\\[{0,2})'+httpRegExp+'( +[^\\]\n]+)?\\]{0,2}$', 'i')
for ( var n in window.urlDecoderEngNames ) {
var ma = rx.exec( oldText ) // result: (whole string)' '[', 'http:...', ' name]'
url = url.replace( RegExp( '(title=|wiki\/)(' + window.urlDecoderEngNames[n] + ':)' ), '$1' + n + ':' );
if( !ma ) return
}
oldText = ma[0]
}
if( ma[3] ) //link with name: automatically add brackets
  newText = simplifyMatched(ma[0], '[', ma[2], ma[3]+']')
else //just url: add closing bracket only if there is leading bracket
  newText = simplifyMatched(ma[0], ma[1], ma[2], ma[1]?']':'')


if( oldText == newText ) return
// try converting to internal link
tbox.textSelection( 'setSelection', {start: caretPos - oldText.length, end: caretPos} )
var linkOrArr, link, isIwTemplate;
// trailing | or }} could mean a part of a template, skip to be safe
if ( !decodingFailed && !/(\}\}|\|)$/.test( url ) ) {
// FIXME: restructure
linkOrArr = toWikilink( url );
if ( linkOrArr !== null && typeof linkOrArr === 'object' ) {
isIwTemplate = linkOrArr[0];
link = linkOrArr[1];
} else {
link = linkOrArr;
}
}


}
if ( isIwTemplate ) {
return link.replace( '||', '|' + ( name ? name : '' ) + '|' );
}


//replace text
// user-defined function
tbox.textSelection( 'encapsulateSelection', {replace:true, peri:newText} )
if ( window.urlDecoderCustom ) {
url = urlDecoderCustom( url );
if ( !/^(https?:\/\/|\{\{)/.test( url ) ) {
link = url; // was converted to internal link
isIwTemplate = false;
}
}


//end of main code
// return internal link
return
if ( link ) {
link = link.replace( /%(3f|26|22)/ig, decodeURIComponent ); // decode ?&"
if ( ( mw.config.get( 'wgNamespaceNumber' ) === 0 || mw.config.get( 'wgNamespaceNumber' ) == 14 ) && isBeforeCursor ) {
link = link.replace( /^:/, '' ); // probably user adding
// interwiki
}
return '[[' + link + ( name ? '|' + name : '' ) + ']]';
}


// or return external link
if ( typeof name == 'string' ) {
if ( isBeforeCursor ) {
url = url.replace( /''/g, '%27%27' ); // techically ''
// should
// stop URL, but more
// likely it's part of
// it
}
return '[' + url + ( name ? ' ' + name : '' ) + ']'; // empty
// name
} else {
return url;
}


}


function toWikilink( url ) { // 'http://xx.wikipedia.org/wiki/YY' ->
// xx:YY


// add bugzilla to user-defined prefixes
var urlDecoderPrefixes = $.extend( window.urlDecoderPrefixes, {
'https://bugzilla.wikimedia.org/show_bug.cgi?id=': 'mediazilla',
'wikidata.org/wiki/': 'd',
'phabricator.wikimedia.org/': 'phab',
'translatewiki.net/wiki/': 'translatewiki',
'ru.wikimedia.org/wiki/': 'wmru'
} );


//---FUNCTIONS
// apply user-defined prefixes
for ( var key in urlDecoderPrefixes ) {
if ( url.toLowerCase().indexOf( key ) !== -1 ) {
return urlDecoderPrefixes[key] + ':' + url.substring( url.indexOf( key ) + key.length );
}
}


// check if we can convert to internal link with WM prefixes
var ma = /^(https?:\/\/[^\/]+)\/wiki\/([^?]+)$/.exec( url ); // 1:'http://domain.org'
// 2:part
// after
// /wiki/
if ( !ma ) {
return null;
}
var linkPrefix = WMPrefixes( ma[1] );
if ( !linkPrefix ) {
return null;
}


function simplifyMatched(str, bracket, url, rest){//arguments: (whole string), '[', url, ' name]'
var title = decodeAnchor( ma[2] );
if ( mw.config.get( 'wgServerName' ) === 'ru.wikipedia.org' &&
// namespaces: main, "Portal", "Incubator"
[ 0, 100, 102 ].indexOf( mw.config.get( 'wgNamespaceNumber' ) ) !== -1 &&
linkPrefix[0] === 'w' &&
linkPrefix[1] !== 'ru'
) {
title = title.replace( /%(3f|26|22)/ig, decodeURIComponent ); // decode ?&"
selectionRange = [ 5, 5 + title.length ];
return [ true, '{{iw|' + title + '||' + linkPrefix[1] + '|' + title + '}}' ];
} else {
// convert to internal
var prefixes = '';
if ( linkPrefix[0] && ( linkPrefix[0] !== localPrefix[0] ) ) {
prefixes = linkPrefix[0];
}
if ( linkPrefix[1] && ( linkPrefix[1] !== localPrefix[1] ) ) {
prefixes += ':' + linkPrefix[1];
}
if ( prefixes || isColonNeeded( title ) ) {
prefixes += ':'; // colon after prefix or leading colon on
// cat/file link
}
return prefixes + title;
}


if( !bracket ){//no brackets, just url
}
  var trail = RegExp(
    '['
      + ',;\\\\\.:!\\?' //trailing punctuation, per Parser.php
      + ( /\(/.test(url) ? '' : '\\)' ) //also closing bracket without opening bracket
      + ']+$'
      + "|''+$" //or possible bold/italic at the end of url
    )
    .exec( url ) 
  if( trail ){
    url = url.substring( 0, url.length - trail[0].length ) //move these out of url
  }
  return decodeUrl(url) + str.substring(url.length)


}else if( rest ){ //both brackets and possibly name
function decodeAnchor( link ) { // simplify internal link: replace %20
  return decodeUrl(url, rest.replace(/\]+$|^ +| +$/g,'')) //trim ending brackets and spaces in 'name]'
// and _
// then decode anchor
link = link.replace( /(_|%20)/g, ' ' ).replace( /^ +| +$/g, '' );
var parts = link.split( '#' );
if ( parts.length !== 2 ) {
return link; // no anchor
}
var anchor = parts[1], hidIdx = -1, hidden = [];
// decode 4, 3 and 2-byte: http://en.wikipedia.org/wiki/UTF-8
anchor = anchor.replace( /\.F[0-4]\.[89AB][\dA-F]\.[89AB][\dA-F]\.[89AB][\dA-F]/g, deChar );
anchor = anchor.replace( /\.E[\dA-F]\.[89AB][\dA-F]\.[89AB][\dA-F]/g, deChar );
anchor = anchor.replace( /\.[CD][\dA-F]\.[89AB][\dA-F]/g, deChar );
anchor = anchor
.replace(
// hide IPs
/(?:^|[^0-9A-F\.])(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/,
function( s ) {
hidden[++hidIdx] = s;
return '\x01' + hidIdx + '\x02';
} );
// decode 1-byte chars: all symbols except -.:_ and []{} prohibited
// in
// links
anchor = anchor.replace( /\.[2-7][0-9A-F]/g, function( hhh ) {
var ch = deChar( hhh );
if ( '!"#$%&\'()*+,/;<=>?@\\^`~'.indexOf( ch ) >= 0 ) {
return ch;
} else {
return hhh;
}
} );
// unhide IPs and return
for ( var i = hidIdx; i >= 0; i-- ) {
anchor = anchor.replace( '\x01' + i + '\x02', hidden[i] );
}
if ( anchor.indexOf( "''" ) !== -1 ) {
return link; // cannot have double '' in link
} else {
return parts[0] + '#' + anchor;
}


}else{
function deChar( ss ) {
  return str //probably broken wikicode in selected text
try {
}
ss = decodeURIComponent( ss.replace( /\.([0-9A-F][0-9A-F])/g, '%$1' ) );
} catch ( e ) {
}
}
return ss;
}
}


function WMPrefixes( url ) { // http: //en.wikipedia.org/wiki/... ->
// [
// 'w', 'en']
var dd = /^https?:\/\/([a-z\.]+)\.org/.exec( url.toLowerCase() );
if ( !dd ) {
return null;
}
dd = dd[1].split( '.' ); // domains, e.g. ['en','wikipedia']
if ( dd.length > 3 ) {
return null; // too many subdomains
}


var lang = '', proj = '', domain = dd.pop(), subdomain = dd.pop();
if ( subdomain === 'm' ) {
subdomain = dd.pop();
} else if ( subdomain === 'www' ) {
subdomain = '';
}


function decodeUrl(url, name){ //url -> %-decoded -> [[link|name]] (if possible); name is optional
if ( domain == 'wikimedia' ) { // *.wikimedia.org
if ( !subdomain ) {
proj = 'foundation';
} else if ( wmSubDomains.test( subdomain ) ) {
proj = subdomain;
} else {
return null;
}


var decodingFailed //need to skip some strange percent-encoded URIs
} else if ( ( proj = wmDomain[domain] ) && !subdomain ) { // mediawiki.org
url = unSecure(url)
// &
// wikimediafoundation.org
// done: proj is set


//percent-decoding
} else if ( proj = wmDomainM[domain] ) { // multi-lang domains
if( url.indexOf('%') != -1 )
if ( !subdomain ) {
try {
// done: e.g. 'wikisource.org'
  url = decodeURI(url)
} else if ( proj == 'w' && subdomain == 'test' ) {
  url = url.replace(/%(3B|2F|2C|3A)/g, decodeURIComponent) //decode ;/,:
proj = 'testwiki';
  url = url.replace(/[ <>"\[\]\n\r|]/g, encodeURIComponent) //" some disallowed chars, and pipe can screw template params
} else if ( subdomain.length >= 2 ) {
} catch(e){
lang = subdomain;
  decodingFailed = true
} else {
}
return null;
}


if( isBeforeCursor ) //user-defined conversion to eng keywords
} else {
  for( var n in window.urlDecoderEngNames )
return null; // unrecognized domain
    url = url.replace(RegExp('(title=|wiki\/)('+urlDecoderEngNames[n]+':)'), '$1' + n + ':')
}


//try converting to internal link
return [ proj, lang ];
if( !decodingFailed && !/(\}\}|\|)$/.test(url) ) //trailing | or }} could mean a part of a template, skip to be safe
}
  var link = toWikilink(url)


//user-defined function
function unSecure( url ) {
if( window.urlDecoderCustom ){
var mm = /https:\/\/secure\.wikimedia\.org\/(\w+)\/(\w+)\/([^\]\|\n\r ]+)/i.exec( url );
    url = urlDecoderCustom(url)
if ( !mm ) {
    if( ! /^(https?:\/\/|\{\{)/.test(url) ) link = url //was converted to internal link
return url;
}
}
var domain = mm[1].toLowerCase(), sub = mm[2].toLowerCase();


//return internal link
if ( !wmDomainM[domain] ) {
if( link ){
return url; // domain not recognized
  link = link.replace(/%(3f|26|22)/ig, decodeURIComponent) //decode ?&"
}
    if( (wgNamespaceNumber==0 || wgNamespaceNumber==14) && isBeforeCursor )
      link = link.replace(/^:/,'') //probably user adding interwiki
  return '[\[' + link + (name?'|'+name:'') + ']]'
}


//or return external link
if ( domain == 'wikipedia' ) { // handle some special cases
if( typeof name == 'string' ){
switch ( sub ) {
  if( isBeforeCursor ) url = url.replace(/''/g,'%27%27') //techically '' should stop URL, but more likely it's part of it
case 'mediawiki':
  return '[' + url + (name?' '+name:'') + ']' //empty name
sub = 'www';
}else{
domain = 'mediawiki';
  return url
break;
}
case 'foundation':
sub = '';
domain = 'wikimediafoundation';
break;
case 'sources':
sub = '';
domain = 'wikisource';
break;
default:
if ( wmSubDomains.test( sub ) ) {
domain = 'wikimedia'; // .../wikipedia/meta ->
// meta.wikimedia.org
}
// otherwise: consider it language: .../wikipedia/en
}
}


}
return 'http://' + ( sub ? sub + '.' : '' ) + domain + '.org/' + mm[3];
}


function isColonNeeded( pg ) {
if ( !/:/.test( pg ) ) {
return false;
}
if ( !colonNS ) { // define list of all possible category and file
// namespaces
var list = [ 'file', 'category' ]; // canonical aliases
for ( var name in mw.config.get( 'wgNamespaceIds' ) ) {
if ( ( mw.config.get( 'wgNamespaceIds' )[name] == 6 || mw.config.get( 'wgNamespaceIds' )[name] == 14 ) && $.inArray( name, list ) == -1 ) {
list.push( name );
}
}
colonNS = RegExp( '^(' + list.join( '|' ) + ') *:', 'i' );
}
return colonNS.test( $.trim( pg ) );
}


};


function toWikilink(url){ // 'http://xx.wikipedia.org/wiki/YY'  ->  xx:YY
registerTool( {
 
name: 'urldecoder',
//add bugzilla to user-defined prefixes
position: 200,
urlDecoderPrefixes = $.extend( window.urlDecoderPrefixes,
title: 'Раскодировать URL',
  { 'https://bugzilla.wikimedia.org/show_bug.cgi?id=' : 'mediazilla' } )
label: 'Раскодировать URL перед курсором или все URL в выделенном тексте',
 
callback: urlDecoderRun,
//apply user-defined prefixes
classic: {
for( var key in urlDecoderPrefixes )
icon: '//upload.wikimedia.org/wikipedia/commons/0/01/Link_go_remake.png',
  if( url.toLowerCase().indexOf(key) != -1 )
},
      return urlDecoderPrefixes[key] + ':' + url.substring( url.indexOf(key) + key.length )
visual: {
 
icon: '//upload.wikimedia.org/wikipedia/commons/thumb/3/3d/URL_decoder_VE_icon.svg/20px-URL_decoder_VE_icon.svg.png',
//check if we can convert to internal link with WM prefixes
modes: [ 'source' ],
var ma = /^(https?:\/\/[^\/]+)\/wiki\/([^?]+)$/.exec( url )// 1:'http://domain.org'  2:part after /wiki/
},
if( !ma ) return null
} );
var linkPrefix = WMPrefixes( ma[1] )
} );
if( !linkPrefix) return null
 
//convert to internal
var title = decodeAnchor( ma[2] )
var prefixes = ''
if( linkPrefix[0] && (linkPrefix[0] != localPrefix[0]) ) prefixes = linkPrefix[0]
if( linkPrefix[1] && (linkPrefix[1] != localPrefix[1]) ) prefixes += ':' + linkPrefix[1]
if( prefixes || isColonNeeded(title) ) prefixes += ':' //colon after prefix or leading colon on cat/file link
return prefixes + title
 
}
 
 
 
 
function decodeAnchor(link){//simplify internal link: replace %20 and _ then decode anchor
link = link.replace(/(_|%20)/g, ' ').replace(/^ +| +$/g, '')
var parts = link.split('#')
if( parts.length != 2 ) return link //no anchor
var anchor = parts[1], hidIdx = -1, hidden = []
//decode 4, 3 and 2-byte: http://en.wikipedia.org/wiki/UTF-8
anchor = anchor.replace(/\.F[0-4]\.[89AB][\dA-F]\.[89AB][\dA-F]\.[89AB][\dA-F]/g, deChar)
anchor = anchor.replace(/\.E[\dA-F]\.[89AB][\dA-F]\.[89AB][\dA-F]/g, deChar)
anchor = anchor.replace(/\.[CD][\dA-F]\.[89AB][\dA-F]/g, deChar)
anchor = anchor.replace( //hide IPs
/(?:^|[^0-9A-F\.])(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/,
  function(s){ hidden[++hidIdx] = s; return '\x01' + hidIdx + '\x02' }
)
//decode 1-byte chars: all symbols except  -.:_  and []{} prohibited in links
anchor = anchor.replace(/\.[2-7][0-9A-F]/g, function(hhh){
  var ch = deChar(hhh)
  if( '!"#$%&\'()*+,/;<=>?@\\^`~'.indexOf(ch) >= 0 ) return ch
  else return hhh
})
//unhide IPs and return
for( var i=hidIdx; i>=0; i-- ) anchor = anchor.replace('\x01'+i+'\x02', hidden[i])
if( anchor.indexOf("''") != -1 ) return link //cannot have double '' in link
else return parts[0] + '#' + anchor
 
function deChar(ss){
  try{ss = decodeURIComponent(ss.replace(/\.([0-9A-F][0-9A-F])/g, '%$1'))} catch(e){}
  return ss
}
}
 
 
 
 
 
function WMPrefixes(url){  // http: //en.wikipedia.org/wiki/...  -> [ 'w',  'en']
 
var dd = /^https?:\/\/([a-z\.]+)\.org/.exec( url.toLowerCase() )
if( !dd ) return null
dd = dd[1].split('.') //domains, e.g. ['en','wikipedia']
if( dd.length > 2 ) return null //too many subdomains, possibly mobile site XX.m.wikipedia.org/
 
var lang = '', proj = '', domain = dd.pop(), subdomain = dd.pop()
if( subdomain == 'www' ) subdomain = ''
 
if( domain == 'wikimedia' ){  // *.wikimedia.org
  if( !subdomain )
    proj = 'foundation'
  else if( wmSubDomains.test(subdomain) )
    proj = subdomain
  else
    return null
 
}else if( (proj = wmDomain[domain]) && !subdomain ){ // mediawiki.org & wikimediafoundation.org
    //done: proj is set
 
}else if( proj = wmDomainM[domain] ){ //multi-lang domains
  if( !subdomain );
    //done: e.g. 'wikisource.org'
  else if( proj == 'w' && subdomain == 'test' )
    proj = 'testwiki'
  else if( subdomain.length >= 2 )
    lang = subdomain
  else
    return null
 
}else return null //unrecognized domain
 
return [proj, lang]
 
}
 
 
 
function unSecure(url){
var mm = /https:\/\/secure\.wikimedia\.org\/(\w+)\/(\w+)\/([^\]\|\n\r ]+)/i.exec( url )
if( !mm) return url
var domain = mm[1].toLowerCase(), sub = mm[2].toLowerCase()
 
if( ! wmDomainM[domain] ) return url //domain not recognized
if( domain == 'wikipedia' ) //handle some special cases
  switch( sub ){
    case 'mediawiki':  sub = 'www'; domain = 'mediawiki'; break
    case 'foundation': sub = ''; domain = 'wikimediafoundation'; break
    case 'sources':    sub = ''; domain = 'wikisource'; break
    default:
      if( wmSubDomains.test(sub) )  domain = 'wikimedia' // .../wikipedia/meta -> meta.wikimedia.org
      //otherwise: consider it language: .../wikipedia/en
  }
return 'http://' + (sub ? sub + '.' : '') + domain + '.org/' + mm[3]
       
}
 
function isColonNeeded(pg){
if( ! /:/.test(pg) ) return false
if( ! colonNS ){ //define list of all possible category and file namespaces
    var list = ['file', 'category'] //canonical aliases
    //if( !window.wgNamespaceIds ) return  alert('Warning: wgNamespaceIds not defined, old MediaWiki version?');
    for( var name in wgNamespaceIds )
      if( (wgNamespaceIds[name]==6 || wgNamespaceIds[name]==14) && $.inArray(name, list) == -1 )
          list.push(name)
    colonNS = RegExp( '^(' + list.join('|') + ') *:', 'i')
}
return colonNS.test( $.trim(pg) )
}
}

Текущая версия от 05:22, 3 ноября 2021

/**
 * [[ВП:Гаджеты/Упрощение ссылок]]
 */

mw.loader.using( 'ext.gadget.registerTool' ).done( function () {
	window.urlDecoderRun = function ( input ) { // main function

		// 2nd-lvl WMF domains; old secure link: .../wikipedia/mediawiki,
		// .../wikipedia/foundation
		var wmDomain = {
			mediawiki: 'mw',
			wikidata: 'd',
			wikisource: 's:mul',
			wikimediafoundation: 'foundation'
		};

		/*
		 * 2nd-lvl WMF domains with multiple languages; old secure link:
		 * wikinews/en
		 */
		var wmDomainM = {
			wikipedia: 'w',
			wikibooks: 'b',
			wikinews: 'n',
			wikiquote: 'q',
			wikisource: 's',
			wikiversity: 'v',
			wikivoyage: 'voy',
			wiktionary: 'wikt'
		};

		/*
		 * 3rd-lvl WMF domains on .wikimedia.org; for some reason old secure
		 * link is wikipedia
		 */
		var wmSubDomains = /^(meta|commons|incubator|species|strategy|wikitech)$/;

		var newText, oldText, rx, isBeforeCursor = false, colonNS, selectionRange,
			// any chars except []<>" and \n and spaces
			httpRegExp = '(https?:\\/\\/[^\\]\\[\\n\\r<>" ]+)',
			localPrefix = WMPrefixes( unSecure( mw.config.get( 'wgServer' ).replace( /^\/\//, 'http://' ) + mw.config.get( 'wgScript' ) ) ),
			tbox = $( input || '#wpTextbox1' ).focus(),
			caretPos = tbox.textSelection( 'getCaretPosition', {
				startAndEnd: true
			} ),
			selectionStartPos = caretPos[0],
			selectionEndPos = caretPos[1];
			
		if (selectionStartPos !== selectionEndPos) { // there was selection
			oldText = tbox.textSelection( 'getSelection' ); 
			rx = RegExp( '(\\[{0,2})' + httpRegExp + '([^\\]\\[\\n\\r]*?\\]\\]?)?', 'ig' );
			newText = oldText.replace( rx, simplifyMatched );

			if ( window.urlDecoderIntLinks ) {
				var ut = '(' + mw.config.get( 'wgFormattedNamespaces' )[3].replace( / /g, '_' ) + '|user_talk)'; // both
				/* localized and canonical 'user_talk' */
				ut = RegExp( '\\[\\[' + ut.toLowerCase() + ':[^#]+$', 'i' );
				newText = newText.replace( /\[\[[^\]\|\n]+/g, function( lnk ) {
					return ut.test( lnk ) ? lnk : decodeAnchor( lnk ); // skip
					// user_talk,
					// usually
					// found in
					// signatures
				} );
			}
			if ( newText == oldText ) {
				return;
			}
			
		} else { // process all text
				
			var content = tbox.textSelection( 'getContents');
			isBeforeCursor = true;

			// search for http string containing cursor
			rx = new RegExp( '(\\[{0,2})' + httpRegExp + '([^\\]\\[\\n\\r]*?\\]\\]?)?', 'ig' );
			while ((ma = rx.exec( content )) !== null) {
				if ((ma.index <= selectionStartPos) && (selectionStartPos <= (ma.index + ma[0].length))) {
					break;
				}
			}
			
			// (whole
			// string)'
			// '[',
			// 'http:...',
			// '
			// name]'
			if ( !ma ) {
				return;
			}
			oldText = ma[0];
			if ( ma[3] ) { // link with name: automatically add brackets
				newText = simplifyMatched( ma[0], '[', ma[2], ma[3] + ']' );
			} else { // just url: add closing bracket only if there is leading bracket
				newText = simplifyMatched( ma[0], ma[1], ma[2], ma[1] ? ']' : '' );
			}

			if ( oldText == newText ) {
				return;
			}
			selectionStartPos = ma.index;
			selectionEndPos = selectionStartPos + oldText.length;
		}
		
		tbox.textSelection( 'setSelection', {
			start: selectionStartPos,
			end: selectionEndPos
		} );
		// replace text
		tbox.textSelection( 'encapsulateSelection', {
			replace: true,
			peri: newText
		} );
		
		// FIXME: restructure
		if ( selectionRange ) {
			tbox.textSelection( 'setSelection', {
				start: selectionEndPos - oldText.length + selectionRange[0],
				end: selectionEndPos - oldText.length + selectionRange[1]
			} );
		}

		// end of main code
		return;

		// ---FUNCTIONS

		function simplifyMatched( str, bracket, url, rest ) { // arguments:
			// (whole
			// string), '[',
			// url, ' name]'
			if ( !bracket ) {// no brackets, just url
				var trail = RegExp( '[' + ',;\\\\\.:!\\?' + // trailing
				// punctuation,
				// per Parser.php
				( /\(/.test( url ) ? '' : '\\)' ) + // also closing bracket
				// without
				// opening bracket
				']+$' + "|''+$" // or possible bold/italic at the end of url
				).exec( url );
				if ( trail ) {
					url = url.substring( 0, url.length - trail[0].length ); // move
					// these
					// out
					// of
					// url
				}
				return decodeUrl( url ) + str.substring( url.length );

			} else if ( rest ) { // both brackets and possibly name
				return decodeUrl( url, rest.replace( /\]+$|^ +| +$/g, '' ) ); // trim
				// ending
				// brackets
				// and
				// spaces
				// in
				// 'name]'
			} else {
				return str; // probably broken wikicode in selected text
			}
		}

		function decodeUrl( url, name ) { // url -> %-decoded -> [[link|name]]
			// (if
			// possible); name is optional

			var decodingFailed; // need to skip some strange percent-encoded
			// URIs
			url = unSecure( url );

			// percent-decoding
			if ( url.indexOf( '%' ) !== -1 ) {
				try {
					url = decodeURI( url );
					url = url.replace( /%(3B|2F|2C|3A)/g, decodeURIComponent ); // decode
					// ;/,:
					url = url.replace( /[ <>"\[\]\n\r]/g, encodeURIComponent ); // "
					/*
					 * some disallowed chars can screw template params
					 */
				} catch ( e ) {
					decodingFailed = true;
				}
			}

			if ( isBeforeCursor ) { // user-defined conversion to eng keywords
				for ( var n in window.urlDecoderEngNames ) {
					url = url.replace( RegExp( '(title=|wiki\/)(' + window.urlDecoderEngNames[n] + ':)' ), '$1' + n + ':' );
				}
			}

			// try converting to internal link
			var linkOrArr, link, isIwTemplate;
			// trailing | or }} could mean a part of a template, skip to be safe
			if ( !decodingFailed && !/(\}\}|\|)$/.test( url ) ) {
				// FIXME: restructure
				linkOrArr = toWikilink( url );
				if ( linkOrArr !== null && typeof linkOrArr === 'object' ) {
					isIwTemplate = linkOrArr[0];
					link = linkOrArr[1];
				} else {
					link = linkOrArr;
				}
			}

			if ( isIwTemplate ) {
				return link.replace( '||', '|' + ( name ? name : '' ) + '|' );
			}

			// user-defined function
			if ( window.urlDecoderCustom ) {
				url = urlDecoderCustom( url );
				if ( !/^(https?:\/\/|\{\{)/.test( url ) ) {
					link = url; // was converted to internal link
					isIwTemplate = false;
				}
			}

			// return internal link
			if ( link ) {
				link = link.replace( /%(3f|26|22)/ig, decodeURIComponent ); // decode ?&"
				if ( ( mw.config.get( 'wgNamespaceNumber' ) === 0 || mw.config.get( 'wgNamespaceNumber' ) == 14 ) && isBeforeCursor ) {
					link = link.replace( /^:/, '' ); // probably user adding
					// interwiki
				}
				return '[[' + link + ( name ? '|' + name : '' ) + ']]';
			}

			// or return external link
			if ( typeof name == 'string' ) {
				if ( isBeforeCursor ) {
					url = url.replace( /''/g, '%27%27' ); // techically ''
					// should
					// stop URL, but more
					// likely it's part of
					// it
				}
				return '[' + url + ( name ? ' ' + name : '' ) + ']'; // empty
				// name
			} else {
				return url;
			}

		}

		function toWikilink( url ) { // 'http://xx.wikipedia.org/wiki/YY' ->
			// xx:YY

			// add bugzilla to user-defined prefixes
			var urlDecoderPrefixes = $.extend( window.urlDecoderPrefixes, {
				'https://bugzilla.wikimedia.org/show_bug.cgi?id=': 'mediazilla',
				'wikidata.org/wiki/': 'd',
				'phabricator.wikimedia.org/': 'phab',
				'translatewiki.net/wiki/': 'translatewiki',
				'ru.wikimedia.org/wiki/': 'wmru'
			} );

			// apply user-defined prefixes
			for ( var key in urlDecoderPrefixes ) {
				if ( url.toLowerCase().indexOf( key ) !== -1 ) {
					return urlDecoderPrefixes[key] + ':' + url.substring( url.indexOf( key ) + key.length );
				}
			}

			// check if we can convert to internal link with WM prefixes
			var ma = /^(https?:\/\/[^\/]+)\/wiki\/([^?]+)$/.exec( url ); // 1:'http://domain.org'
			// 2:part
			// after
			// /wiki/
			if ( !ma ) {
				return null;
			}
			var linkPrefix = WMPrefixes( ma[1] );
			if ( !linkPrefix ) {
				return null;
			}

			var title = decodeAnchor( ma[2] );
			if ( mw.config.get( 'wgServerName' ) === 'ru.wikipedia.org' &&
				// namespaces: main, "Portal", "Incubator"
				[ 0, 100, 102 ].indexOf( mw.config.get( 'wgNamespaceNumber' ) ) !== -1 &&
				linkPrefix[0] === 'w' &&
				linkPrefix[1] !== 'ru'
			) {
				title = title.replace( /%(3f|26|22)/ig, decodeURIComponent ); // decode ?&"
				selectionRange = [ 5, 5 + title.length ];
				return [ true, '{{iw|' + title + '||' + linkPrefix[1] + '|' + title + '}}' ];
			} else {
				// convert to internal
				var prefixes = '';
				if ( linkPrefix[0] && ( linkPrefix[0] !== localPrefix[0] ) ) {
					prefixes = linkPrefix[0];
				}
				if ( linkPrefix[1] && ( linkPrefix[1] !== localPrefix[1] ) ) {
					prefixes += ':' + linkPrefix[1];
				}
				if ( prefixes || isColonNeeded( title ) ) {
					prefixes += ':'; // colon after prefix or leading colon on
					// cat/file link
				}
				return prefixes + title;
			}

		}

		function decodeAnchor( link ) { // simplify internal link: replace %20
			// and _
			// then decode anchor
			link = link.replace( /(_|%20)/g, ' ' ).replace( /^ +| +$/g, '' );
			var parts = link.split( '#' );
			if ( parts.length !== 2 ) {
				return link; // no anchor
			}
			var anchor = parts[1], hidIdx = -1, hidden = [];
			// decode 4, 3 and 2-byte: http://en.wikipedia.org/wiki/UTF-8
			anchor = anchor.replace( /\.F[0-4]\.[89AB][\dA-F]\.[89AB][\dA-F]\.[89AB][\dA-F]/g, deChar );
			anchor = anchor.replace( /\.E[\dA-F]\.[89AB][\dA-F]\.[89AB][\dA-F]/g, deChar );
			anchor = anchor.replace( /\.[CD][\dA-F]\.[89AB][\dA-F]/g, deChar );
			anchor = anchor
					.replace(
							// hide IPs
							/(?:^|[^0-9A-F\.])(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/,
							function( s ) {
								hidden[++hidIdx] = s;
								return '\x01' + hidIdx + '\x02';
							} );
			// decode 1-byte chars: all symbols except -.:_ and []{} prohibited
			// in
			// links
			anchor = anchor.replace( /\.[2-7][0-9A-F]/g, function( hhh ) {
				var ch = deChar( hhh );
				if ( '!"#$%&\'()*+,/;<=>?@\\^`~'.indexOf( ch ) >= 0 ) {
					return ch;
				} else {
					return hhh;
				}
			} );
			// unhide IPs and return
			for ( var i = hidIdx; i >= 0; i-- ) {
				anchor = anchor.replace( '\x01' + i + '\x02', hidden[i] );
			}
			if ( anchor.indexOf( "''" ) !== -1 ) {
				return link; // cannot have double '' in link
			} else {
				return parts[0] + '#' + anchor;
			}

			function deChar( ss ) {
				try {
					ss = decodeURIComponent( ss.replace( /\.([0-9A-F][0-9A-F])/g, '%$1' ) );
				} catch ( e ) {
				}
				return ss;
			}
		}

		function WMPrefixes( url ) { // http: //en.wikipedia.org/wiki/... ->
			// [
			// 'w', 'en']
			var dd = /^https?:\/\/([a-z\.]+)\.org/.exec( url.toLowerCase() );
			if ( !dd ) {
				return null;
			}
			dd = dd[1].split( '.' ); // domains, e.g. ['en','wikipedia']
			if ( dd.length > 3 ) {
				return null; // too many subdomains
			}

			var lang = '', proj = '', domain = dd.pop(), subdomain = dd.pop();
			
			if ( subdomain === 'm' ) {
				subdomain = dd.pop();
			} else if ( subdomain === 'www' ) {
				subdomain = '';
			}

			if ( domain == 'wikimedia' ) { // *.wikimedia.org
				if ( !subdomain ) {
					proj = 'foundation';
				} else if ( wmSubDomains.test( subdomain ) ) {
					proj = subdomain;
				} else {
					return null;
				}

			} else if ( ( proj = wmDomain[domain] ) && !subdomain ) { // mediawiki.org
				// &
				// wikimediafoundation.org
				// done: proj is set

			} else if ( proj = wmDomainM[domain] ) { // multi-lang domains
				if ( !subdomain ) {
					// done: e.g. 'wikisource.org'
				} else if ( proj == 'w' && subdomain == 'test' ) {
					proj = 'testwiki';
				} else if ( subdomain.length >= 2 ) {
					lang = subdomain;
				} else {
					return null;
				}

			} else {
				return null; // unrecognized domain
			}

			return [ proj, lang ];
		}

		function unSecure( url ) {
			var mm = /https:\/\/secure\.wikimedia\.org\/(\w+)\/(\w+)\/([^\]\|\n\r ]+)/i.exec( url );
			if ( !mm ) {
				return url;
			}
			var domain = mm[1].toLowerCase(), sub = mm[2].toLowerCase();

			if ( !wmDomainM[domain] ) {
				return url; // domain not recognized
			}

			if ( domain == 'wikipedia' ) { // handle some special cases
				switch ( sub ) {
				case 'mediawiki':
					sub = 'www';
					domain = 'mediawiki';
					break;
				case 'foundation':
					sub = '';
					domain = 'wikimediafoundation';
					break;
				case 'sources':
					sub = '';
					domain = 'wikisource';
					break;
				default:
					if ( wmSubDomains.test( sub ) ) {
						domain = 'wikimedia'; // .../wikipedia/meta ->
						// meta.wikimedia.org
					}
					// otherwise: consider it language: .../wikipedia/en
				}
			}

			return 'http://' + ( sub ? sub + '.' : '' ) + domain + '.org/' + mm[3];
		}

		function isColonNeeded( pg ) {
			if ( !/:/.test( pg ) ) {
				return false;
			}
			if ( !colonNS ) { // define list of all possible category and file
				// namespaces
				var list = [ 'file', 'category' ]; // canonical aliases
				for ( var name in mw.config.get( 'wgNamespaceIds' ) ) {
					if ( ( mw.config.get( 'wgNamespaceIds' )[name] == 6 || mw.config.get( 'wgNamespaceIds' )[name] == 14 ) && $.inArray( name, list ) == -1 ) {
						list.push( name );
					}
				}
				colonNS = RegExp( '^(' + list.join( '|' ) + ') *:', 'i' );
			}
			return colonNS.test( $.trim( pg ) );
		}

	};

	registerTool( {
		name: 'urldecoder',
		position: 200,
		title: 'Раскодировать URL',
		label: 'Раскодировать URL перед курсором или все URL в выделенном тексте',
		callback: urlDecoderRun,
		classic: {
			icon: '//upload.wikimedia.org/wikipedia/commons/0/01/Link_go_remake.png',
		},
		visual: {
			icon: '//upload.wikimedia.org/wikipedia/commons/thumb/3/3d/URL_decoder_VE_icon.svg/20px-URL_decoder_VE_icon.svg.png',
			modes: [ 'source' ],
		},
	} );
} );