MediaWiki:Gadget-AuthorityControl.js
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)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* AuthorityControl.js
* Provides a link to various Authority Control tools for some Wikidata statements that
* are not external identifiers.
*
* Original gadget coded by [[User:Ricordisamoa]]
*/
( function ( mw, wb, $ ) {
'use strict';
if ( [ 0, 120, 146 ].indexOf( mw.config.get( 'wgNamespaceNumber' ) ) === -1 || !mw.config.exists( 'wbEntityId' ) ) {
// Only entity pages feature appropriate statements.
return;
}
var PROPERTIES = {},
specialHandlingProperties = [
'P426', // aircraft registration
];
/*
*/
function getGeoHackParams( coord ) {
// TODO: individual scale for every precision
var globes = {
Q2: 'earth',
Q111: 'mars',
Q308: 'mercury',
Q313: 'venus',
Q319: 'jupiter',
Q339: 'pluto',
Q405: 'moon',
Q596: 'ceres',
Q2565: 'titan',
Q3030: 'vesta',
Q3123: 'io',
Q3134: 'callisto',
Q3143: 'europa',
Q3169: 'ganymede',
Q3303: 'enceladus',
Q3322: 'titania',
Q3332: 'oberon',
Q3338: 'umbriel',
Q3343: 'ariel',
Q3352: 'miranda',
Q3359: 'triton',
Q7547: 'phobos',
Q7548: 'deimos',
Q15034: 'mimas',
Q15037: 'hyperion',
Q15040: 'dione',
Q15047: 'tethys',
Q15050: 'rhea',
Q16711: 'eros',
Q17958: 'iapetus',
Q17975: 'phoebe',
Q107556: 'lutetia',
Q158244: 'gaspra'
};
var globeQKey = coord.globe.replace( 'http://www.wikidata.org/entity/', '' );
var globe = globes[ globeQKey ];
return coord.latitude + '_N_' + coord.longitude + '_E_globe:' + globe;
}
/**
* Get the snak value formatted with a link.
*
* @param {number} numericPropertyId Refers to PROPERTIES.
* @param {string} value
*/
function getLinkValueForString( numericPropertyId, value ) {
var linkValue;
switch ( Number( numericPropertyId ) ) {
default:
linkValue = value;
}
return linkValue;
}
function makeLink( numericPropertyId, linkValue, displayText ) {
var linkTemplate = PROPERTIES[ numericPropertyId ];
switch ( Number( numericPropertyId ) ) {
case 426:
if ( linkValue.substring( 0, 1 ) === 'N' ) {
linkTemplate = 'https://registry.faa.gov/AircraftInquiry/Search/NNumberResult?nNumberTxt=$1';
} else if ( linkValue.substring( 0, 2 ) === 'G-' ) {
// FIXME: this is said to be non-functional https://www.wikidata.org/w/index.php?oldid=1808076598#Update_to_FAA_URL
linkTemplate = 'https://www.caa.co.uk/application.aspx?catid=60&pagetype=65&appid=1&mode=detailnosummary&fullregmark=$1';
linkValue = linkValue.substring( 2 );
} else {
return linkValue;
}
break;
case 625:
linkTemplate += '&language=' + mw.config.get( 'wgUserLanguage' );
break;
case 1793:
case 8460:
case 8966:
case 10999:
case 11707:
linkTemplate = PROPERTIES[ numericPropertyId ];
if ( linkTemplate === 'https://regex101.com/?regex=$1' ) {
// Escape the character used as the delimiter, which for
// regex101.com is slashes.
// URL encode the value to avoid problems when a regex contains
// characters with a special meaning in URLs, like & and #.
try {
// try to encode / as \/, if not encoded yet
linkValue = (new RegExp(linkValue)).source;
} catch (error) {
// display anyway
}
linkValue = encodeURIComponent( linkValue );
}
break;
case 233:
case 2017:
case 8533:
case 10718:
linkValue = encodeURIComponent( linkValue );
break;
default:
linkTemplate = PROPERTIES[ numericPropertyId ];
}
var link = linkTemplate.replace( /\$1/g, linkValue );
try {
var prot = (new URL(link)).protocol;
// Disallow javascript links to prevent xss.
// Use URL parser to handle cases with spaces and other bypasses
if (prot === 'javascript:' || prot === 'data:') {
return $( '<span>' ).text( displayText )
}
} catch (error) {
return $( '<span>' ).text( displayText );
}
return $( '<a>' )
.text( displayText )
.attr( 'href', link )
// Show the 'external link' icon:
.addClass( 'external' );
}
function createLinkForString( numericPropertyId, value ) {
var linkValue = getLinkValueForString( numericPropertyId, value );
return makeLink( numericPropertyId, linkValue, value );
}
function createLinkForSnakValue( numericPropertyId, dataValue, displayText ) {
var dataValueType = dataValue.getType(),
value = dataValue.toJSON();
// @fixme shouldn't happen but in case of any unexpected data value types,
// then there should be better error handling here.
var linkValue = '';
if ( dataValueType === 'string' ) {
linkValue = getLinkValueForString( numericPropertyId, value );
} else if ( dataValueType === 'globecoordinate' ) {
linkValue = getGeoHackParams( value );
}
return makeLink( numericPropertyId, linkValue, displayText );
}
function linkSnakView( el, propertySelector, valueSelector ) {
var $propLink = $( el ).find( propertySelector );
var title = $propLink.attr( 'title' );
if ( title ) {
var titleParts = title.split( ':P' ),
numericPropertyId = titleParts[ 1 ];
if ( PROPERTIES.hasOwnProperty( numericPropertyId ) ) {
var $value = $( el ).find( valueSelector ).first(),
$link = createLinkForString( numericPropertyId, $value.text() );
$value.html( $link );
}
}
}
function handleSnak( snak, snakView ) {
if ( !( snak.getType && snak.getType() == 'value' ) ) {
return;
}
var numericPropertyId = snak.getPropertyId().slice( 1 );
if ( !( PROPERTIES.hasOwnProperty( numericPropertyId ) ) ) {
return;
}
var $snakValue = $( snakView ).find( '.wikibase-snakview-value' );
if ( $snakValue.find( '.wikibase-kartographer-caption' ).length ) {
// If this is a Kartographer map, don't mangle the whole snak value,
// but just the caption.
$snakValue = $snakValue.find( '.wikibase-kartographer-caption' );
}
var displayText = extractDisplayText( $snakValue ),
snakLink = createLinkForSnakValue( numericPropertyId, snak.getValue(), displayText );
$snakValue.html( snakLink );
}
function extractDisplayText( $snakValue ) {
var $snakValueClone = $snakValue.clone();
$snakValueClone.children().remove();
return $snakValueClone.text();
}
/**
* Initializes the gadget.
* This procedure needs to be performed as good as possible. jQuery selector usage should be limited
* to a minimum.
*/
function initGadget() {
if ( $.isEmptyObject( PROPERTIES ) ) {
return;
}
$( ':wikibase-statementview' ).each( function () {
var statementview = $.data( this, 'statementview' ),
statement = statementview.value(),
claim = statement.getClaim(),
qualifierGroups = claim.getQualifiers().getGroupedSnakLists();
handleSnak( claim.getMainSnak(), statementview.$mainSnak[ 0 ] );
$( '.wikibase-statementview-qualifiers .wikibase-snaklistview', this ).each( function( i ) {
var qualifiers = qualifierGroups[i].toArray();
$( '.wikibase-snakview', this ).each( function( n ) {
handleSnak( qualifiers[n], this );
} );
} );
} );
$( '.wikibase-referenceview .wikibase-snaklistview-listview' ).each( function () {
linkSnakView( this, '.wikibase-snakview-property > a', '.wikibase-snakview-value' );
} );
}
function getProperties( entity ) {
var api = new mw.Api(),
repoApi = new wb.api.RepoApi( api ),
propertyIds = [],
alreadyLinkedPropertyIds = [];
function addSnak( snak ) {
var snakPropertyId = snak.property,
$firstSnakValue;
if ( snak.snaktype !== 'value' ||
( snak.datavalue.type !== 'string' && snak.datavalue.type !== 'globecoordinate' ) ) {
return;
}
if ( propertyIds.indexOf( snakPropertyId ) !== -1 ) {
return;
}
if ( specialHandlingProperties.indexOf( snakPropertyId ) === -1 ) {
if ( alreadyLinkedPropertyIds.indexOf( snakPropertyId ) !== -1 ) {
return;
}
$firstSnakValue = $( '#' + snakPropertyId ).find( '.wikibase-snakview-variation-valuesnak:first' );
if ( $firstSnakValue.find( '> a:not(.oo-ui-widget)' ).length > 0 || $firstSnakValue.find( '> div.thumb' ).length > 0 ) {
alreadyLinkedPropertyIds.push( snakPropertyId );
return;
}
}
propertyIds.push( snakPropertyId );
}
function analyzeClaims( claims ) {
var prop;
for ( prop in claims ) {
$.each( claims[ prop ], function ( i, claim ) {
addSnak( claim.mainsnak );
$.each( claim.references || [], function ( i, ref ) {
for ( prop in ref.snaks ) {
$.each( ref.snaks[ prop ], function ( i, cl ) {
addSnak( cl );
} );
}
} );
for ( prop in claim.qualifiers || {} ) {
$.each( claim.qualifiers[ prop ], function ( i, cl ) {
addSnak( cl );
} );
}
} );
}
}
analyzeClaims( entity.claims );
$.each( entity.forms || [], function ( _, form ) {
analyzeClaims( form.claims || {} );
} );
$.each( entity.senses || [], function ( _, sense ) {
analyzeClaims( sense.claims || {} );
} );
if ( !propertyIds.length ) {
return $.Deferred().resolve();
}
return repoApi.getEntities( propertyIds, 'claims' )
.then( function ( data ) {
if ( !data || !data.entities ) {
// Unexpected response from the API
return;
}
$.each( data.entities, function ( entityId, entity ) {
if ( entity.datatype === 'external-id' && $.inArray( entity.id, specialHandlingProperties ) === -1 ) {
// No need to format these
return true;
}
if ( entity.datatype === 'commonsMedia' ) {
// No need to format these, but we can't exclude them earlier
// as we don't have the property datatype.
// XXX: This probably also applies to math etc.
return true;
}
$.each( entity.claims, function ( claimId, claims ) {
if ( claimId === 'P1630' ) {
var i, len = claims.length;
for ( i = 0; i < len; i++ ) {
if ( claims[ i ].rank !== 'preferred' ) {
continue;
}
if ( claims[ i ].mainsnak.snaktype === 'value' ) {
PROPERTIES[ entityId.slice( 1 ) ] = claims[ i ].mainsnak.datavalue.value;
}
return false;
}
for ( i = 0; i < len; i++ ) {
if ( claims[ i ].rank !== 'normal' ) {
continue;
}
if ( claims[ i ].mainsnak.snaktype === 'value' ) {
PROPERTIES[ entityId.slice( 1 ) ] = claims[ i ].mainsnak.datavalue.value;
break;
}
}
return false;
}
} );
} );
} );
}
var rendered = $.Deferred(),
loaded = $.Deferred();
$.when(
rendered,
loaded,
$.ready
).then( function () {
initGadget();
} );
mw.hook( 'wikibase.entityPage.entityLoaded' ).add( function ( entity ) {
getProperties( entity ).then( function () {
loaded.resolve();
} );
} );
mw.hook( 'wikibase.entityPage.entityView.rendered' ).add( function () {
rendered.resolve();
} );
}( mediaWiki, wikibase, jQuery ) );