User:Samwilson/CiteTool.js
Appearance
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.
/**
* Adds a reference section button that appears when a reference URL (P854) is
* entered. Clicking it will use Citoid to query the entered URL and extract any
* extra reference fields that it can find.
*
* Install by putting the following in your common.js:
*
* importScript( 'User:Samwilson/CiteTool.js' ); // [[User:Samwilson/CiteTool.js]]
*
* This is Samwilson's fork of User:MichaelSchoenitzer's fork of User:Aude's CiteTool.
*
* @TODO:
* - Compare with Zotero QS tool: https://github.com/UB-Mannheim/zotkat/blob/master/Wikidata%20QuickStatements.js
* - Update URL from what's returned by Citoid.
* - Add i18n.
*/
( function( dv, mw, $ ) {
'use strict';
function CiteTool( configUrl ) {
this.configUrl = configUrl;
this.config = null;
this.citoidClient = new CitoidClient();
this.citeToolReferenceEditor = null;
this.citeToolAutofillLinkRenderer = null;
}
CiteTool.prototype.init = function() {
var self = this;
if ( !mw.config.exists( 'wbEntityId' ) ) {
return;
}
$( '.wikibase-entityview' )
.on( 'referenceviewafterstartediting', function( e ) {
self.initAutofillLink( e.target );
} );
// @fixme the event also fires for other changes, like editing qualifiers
$( '.wikibase-entityview' )
.on( 'snakviewchange', function( e ) {
self.initAutofillLink( e.target );
} );
};
CiteTool.prototype.getConfig = function() {
var dfd = $.Deferred();
$.ajax( {
url: this.configUrl,
dataType: 'json',
success: function( config ) {
dfd.resolve( config );
},
error: function( result ) {
console.error( 'Error loading citoid config from ' + this.configUrl );
}
} );
return dfd.promise();
};
CiteTool.prototype.initAutofillLink = function( target ) {
var self = this;
if ( this.config === null ) {
this.getConfig()
.done( function( config ) {
self.config = config;
self.citeToolReferenceEditor = new CiteToolReferenceEditor( config );
self.citeToolAutofillLinkRenderer = new CiteToolAutofillLinkRenderer(
config,
self.citoidClient,
self.citeToolReferenceEditor
);
self.checkReferenceAndAddAutofillLink( target );
} );
} else {
var $refViews = $( target ).closest( '.wikibase-referenceview' );
self.checkReferenceAndAddAutofillLink( $refViews[0] );
}
};
CiteTool.prototype.checkReferenceAndAddAutofillLink = function( target ) {
if ( $( target ).find( '.wikibase-citetool-autofill' ).length > 0 ) {
return;
}
var reference = this.getReferenceFromView( target );
if ( reference && this.getLookupSnakProperty( reference ) !== null ) {
this.citeToolAutofillLinkRenderer.renderLink( target );
}
};
CiteTool.prototype.getReferenceFromView = function( referenceView ) {
// not a reference view change
if ( referenceView === undefined ) {
return null;
}
var refView = $( referenceView ).data( 'referenceview' );
return refView && refView.value && refView.value();
};
CiteTool.prototype.getLookupSnakProperty = function( reference ) {
var snaks = reference.getSnaks(),
lookupProperties = this.getLookupProperties(),
lookupProperty = null;
snaks.each( function( k, snak ) {
var propertyId = snak.getPropertyId();
if ( lookupProperties.indexOf( propertyId ) !== -1 ) {
if ( lookupProperty === null ) {
lookupProperty = propertyId;
}
}
} );
return lookupProperty;
};
CiteTool.prototype.getLookupProperties = function() {
var properties = [];
if ( this.config.properties ) {
properties = Object.keys( this.config.properties );
}
return properties;
};
function CiteToolAutofillLinkRenderer( config, citoidClient, citeToolReferenceEditor ) {
this.config = config;
this.citoidClient = citoidClient;
this.citeToolReferenceEditor = citeToolReferenceEditor;
}
CiteToolAutofillLinkRenderer.prototype.renderLink = function( referenceView ) {
var self = this;
var $div = $( '<div>' )
.addClass( 'wikibase-toolbar-button wikibase-citetool-autofill' )
.css( { 'margin': '0 .5em', 'text-align': 'center' } )
.append(
$( '<a>' )
.text( 'Autofill Details' )
.attr( {
'class': 'wikibase-referenceview-autofill',
'title': 'Search and automatically add information about source'
} )
.on( 'click', function( e ) {
e.preventDefault();
self.onAutofillClick( e.target );
} )
);
$( referenceView ).append( $div );
};
CiteToolAutofillLinkRenderer.prototype.getReferenceFromView = function( $referenceView ) {
// not a reference view change
if ( $referenceView === undefined ) {
return null;
}
var refView = $referenceView.data( 'referenceview' );
return refView.value();
};
CiteToolAutofillLinkRenderer.prototype.getLookupSnakProperty = function( reference ) {
var snaks = reference.getSnaks(),
lookupProperties = this.getLookupProperties(),
lookupProperty = null;
snaks.each( function( k, snak ) {
var propertyId = snak.getPropertyId();
if ( lookupProperties.indexOf( propertyId ) !== -1 ) {
if ( lookupProperty === null ) {
lookupProperty = propertyId;
}
}
} );
return lookupProperty;
};
CiteToolAutofillLinkRenderer.prototype.getLookupProperties = function() {
var properties = [];
if ( this.config.properties ) {
properties = Object.keys( this.config.properties );
}
return properties;
};
CiteToolAutofillLinkRenderer.prototype.onAutofillClick = function( target ) {
var $referenceView = $( target ).closest( '.wikibase-referenceview' ),
reference = this.getReferenceFromView( $referenceView ),
self = this;
if ( reference === null ) {
return;
}
var value = this.getLookupSnakValue( reference );
var progressbar = new OO.ui.ProgressBarWidget( {
progress: false
} );
$referenceView.append(progressbar.$element);
this.citoidClient.search( value )
.done( function( data ) {
progressbar.$element.remove();
if ( data[0] ) {
self.citeToolReferenceEditor.addReferenceSnaksFromCitoidData(
data[0],
$referenceView
);
if ( data[0].url ) {
self.addReferenceSnaksFromUrl( data[0].url, $referenceView );
}
}
} );
};
CiteToolAutofillLinkRenderer.prototype.addReferenceSnaksFromUrl = function ( url, $referenceView ) {
var self = this;
var sparqlQuery = new SparqlQuery( 'P856', url, 'url' );
sparqlQuery.getResults().then( function ( data ) {
if ( data.results.bindings[0] === undefined ) {
return;
}
// @TODO Check for publisher property on this item.
var qid = data.results.bindings[0].item.value.substr( 'http://www.wikidata.org/entity/'.length );
var refView = $referenceView.data( 'referenceview' );
var item = self.citeToolReferenceEditor.getItemValueSnak( 'P123', qid );
var lv = self.citeToolReferenceEditor.getReferenceSnakListView( refView );
lv.addItem( item );
lv.startEditing();
refView._trigger( 'change' );
} );
}
CiteToolAutofillLinkRenderer.prototype.getLookupSnakValue = function( reference ) {
var value = null,
lookupProperties = this.getLookupProperties();
reference.getSnaks().each( function( k, snak ) {
var propertyId = snak.getPropertyId();
if ( lookupProperties.indexOf( propertyId ) !== -1 ) {
value = snak.getValue().getValue();
}
} );
return value;
};
function CiteToolReferenceEditor( config ) {
this.config = config;
this.citoidClient = new CitoidClient();
}
CiteToolReferenceEditor.prototype.addReferenceSnaksFromCitoidData = function( data, $referenceView ) {
var refView = $referenceView.data( 'referenceview' ),
lv = this.getReferenceSnakListView( refView ),
usedProperties = refView.value().getSnaks().getPropertyOrder(),
self = this,
addedSnakItem = false,
addedUnhandled = false;
var $listUnhandled = $( '<ul>' ).addClass( 'citetool-unhandled-props' );
$.each( data, function( key, val ) {
var property = self.getPropertyForCitoidData( key );
if ( property === null ) {
$listUnhandled.append( $( '<li>' ).append( $( '<strong>' ).text( key + ': ' ), JSON.stringify( val ) ) );
addedUnhandled = true;
return;
}
var propertyId = property.id;
var propertyType = property.valuetype;
if ( propertyId !== null && usedProperties.indexOf( propertyId ) !== -1 ) {
return;
}
if ( key === 'language' ) {
val = self.getLanguageItem( data );
}
if ( !val ) {
return;
}
// ISSN P236, array of strings
if ( key === 'ISSN' ) {
for ( var issn of val ) {
lv.addItem( self.getStringValueSnak( 'P236', issn ) );
addedSnakItem = true;
}
return;
}
switch ( propertyType ) {
case 'item':
lv.addItem( self.getItemValueSnak(propertyId, val ) );
addedSnakItem = true;
break;
case 'monolingualtext':
val = val.replace(/\s+/g, " ");
lv.addItem( self.getMonolingualValueSnak(
propertyId,
val,
self.getTitleLanguage( data )
) );
if ( !self.getLanguage( data ) ) {
mw.notify( "Looking up language failed, guessing english.", { title: "CiteTool", type: "warn", autohide: true } );
}
addedSnakItem = true;
break;
case 'string':
val = val.replace(/\s+/g, " ");
lv.addItem( self.getStringValueSnak(
propertyId,
val,
data
) );
addedSnakItem = true;
break;
case 'date':
try {
lv.addItem( self.getDateSnak( propertyId, val ) );
addedSnakItem = true;
} catch (e) { }
break;
case 'author':
// author name string (P2093)
for ( var author of val ) {
var authorName = author.map( function( a ){ return a.trim(); } ).join( ' ' );
lv.addItem( self.getStringValueSnak( propertyId, authorName ) );
addedSnakItem = true;
}
break;
default:
break;
}
} );
if ( addedSnakItem === true ) {
lv.startEditing();
refView._trigger( 'change' );
}
if ( addedUnhandled ) {
$referenceView.append( $( '<strong>' ).text( 'Other citation data:' ), $listUnhandled );
}
};
CiteToolReferenceEditor.prototype.getReferenceSnakListView = function( refView ) {
var refListView = refView.$listview.data( 'listview' ),
snakListView = refListView.items(),
snakListViewData = snakListView.data( 'snaklistview' ),
listView = snakListViewData.$listview.data( 'listview' );
return listView;
};
CiteToolReferenceEditor.prototype.getPropertyForCitoidData = function( key ) {
if ( this.config.zoteroProperties[key] ) {
return this.config.zoteroProperties[key];
}
return null;
};
CiteToolReferenceEditor.prototype.getLanguage = function( data ) {
if ( data.language ) {
var languageCode = data.language.toLowerCase();
if ( languageCode in mw.config.values.wgULSLanguages ) {
return languageCode;
}
languageCode = languageCode.split('-')[0];
if( languageCode in mw.config.values.wgULSLanguages ) {
return languageCode;
}
}
return null;
};
CiteToolReferenceEditor.prototype.getTitleLanguage = function( data ) {
var lang = this.getLanguage( data );
if( lang ) {
return lang;
}
return 'en';
};
CiteToolReferenceEditor.prototype.getLanguageItem = function( data ) {
var langcode = this.getLanguage(data);
if ( langcode && this.config.languages[langcode] ) {
return this.config.languages[langcode];
}
return null;
};
CiteToolReferenceEditor.prototype.getMonolingualValueSnak = function( propertyId, title, languageCode ) {
return new wb.datamodel.PropertyValueSnak(
propertyId,
new dv.MonolingualTextValue( languageCode, title )
);
};
CiteToolReferenceEditor.prototype.getStringValueSnak = function( propertyId, string ) {
return new wb.datamodel.PropertyValueSnak(
propertyId,
new dv.StringValue( string )
);
};
CiteToolReferenceEditor.prototype.getItemValueSnak = function( propertyId, item ) {
return new wb.datamodel.PropertyValueSnak(
propertyId,
new wb.datamodel.EntityId(item)
);
};
CiteToolReferenceEditor.prototype.getDateSnak = function( propertyId, dateString ) {
var timestamp = dateString + 'T00:00:00Z';
return new wb.datamodel.PropertyValueSnak(
propertyId,
new dv.TimeValue( timestamp )
);
};
/**
* Client for fetching data from the Citoid API.
* @class
*/
function CitoidClient() {
}
/**
* @param {string} value Search term.
* @return {Promise}
*/
CitoidClient.prototype.search = function( value ) {
var dfd = $.Deferred(),
baseUrl = 'https://en.wikipedia.org/api/rest_v1/data/citation',
format = 'mediawiki',
url = baseUrl + '/' + format + '/' + encodeURIComponent( value );
$.ajax( {
method: 'GET',
url: url,
data: {}
} )
.done( function( citoidData ) {
dfd.resolve( citoidData );
} )
.fail(function( data ) {
mw.notify("Lookup failed.", {title: "CiteTool", type: "error", autohide: true});
// Add lookup-date anyway!
var date = new Date();
dfd.resolve( [{"accessDate": date.toISOString().slice(0,10)}] );
});
return dfd.promise();
};
function SparqlQuery( propertyId, value, type ) {
this.propertyId = propertyId;
this.value = value;
this.type = type;
}
/**
* @return {Promise}
*/
SparqlQuery.prototype.getResults = function () {
var self = this;
var queryParts = [];
if ( this.type === 'url' ) {
this.hostVariants( this.value ).forEach( function ( url ) {
queryParts.push( '{ ?item wdt:' + self.propertyId + ' <' + url + '> }' );
} );
} else {
queryParts.push( '{ ?item wdt:' + this.propertyId + ' "' + this.value + '" }' );
}
var query = 'SELECT ?item WHERE { ' + queryParts.join( ' UNION ' ) + ' } LIMIT 1';
return $.ajax( {
method: 'GET',
url: 'https://query.wikidata.org/sparql?format=json&query=' + encodeURIComponent( query ),
data: {}
} );
}
/**
* @param {string} urlString
* @return {array} Array of variants of the host from the given URL.
*/
SparqlQuery.prototype.hostVariants = function ( urlString ) {
var hosts = [];
var url = new URL( urlString );
hosts.push ( url.hostname );
// Add/remove www.
var parts = url.hostname.split( '.' );
if ( parts[0] === 'www' ) {
hosts.push( parts.splice( 1 ).join( '.' ) );
} else {
hosts.push( 'www.' + url.hostname );
}
// Add/remove trailing slash.
hosts.forEach( function ( host ) {
if ( host.charAt( 0 ) === '/') {
hosts.push( host.substring( 0, host.length - 1 ) );
} else {
hosts.push( host + '/' );
}
} );
// Add http
var hostsFinal = [];
hosts.forEach( function ( host ) {
hostsFinal.push( 'http://' + host );
hostsFinal.push( 'https://' + host );
} );
return hostsFinal;
}
mw.loader.using( [ 'wikibase', 'wikibase.datamodel' ], function( require ) {
console.log('Running user script User:Samwilson/CiteTool.js');
wb.datamodel = require( 'wikibase.datamodel' );
var citeTool = new CiteTool( mw.config.get( 'wgScript' ) + '?title=User:Samwilson/CiteProperties.json&action=raw&ctype=text/json' );
citeTool.init();
} );
})( window.dataValues || [], mediaWiki, jQuery );