User:Premeditated/CiteTool.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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: 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:Premeditated/CiteTool.js' ); // [[User:Premeditated/CiteTool.js]]
 *
 * This is Premeditated's fork of User: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( wb, 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
                    );
                }
            } );
    };

    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;

        console.log( data );

        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;
            }
            
            if ( key === 'author' ) {
                for ( var author of val ) {
                	lv.addItem( self.getStringValueSnak( 'P2093', author.join(' ').trim() ) );
                    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;
                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]; // Get IETF language code root as fallback
            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' } );
            console.error( 'Failed to fetch Citoid data from ' + url, data );
            // Add lookup-date anyway!
            var date = new Date();
            dfd.resolve( [{"accessDate": date.toISOString().slice(0,10)}] );
        });

        return dfd.promise();
    };

    mw.loader.using( [ 'wikibase', 'wikibase.datamodel' ], function( require ) {
        wb.datamodel = require( 'wikibase.datamodel' );
        var citeTool = new CiteTool( 'https://www.wikidata.org/w/index.php?title=User:Premeditated/CiteProperties.json&action=raw&ctype=text/json' );
        citeTool.init();
    });

})( wikibase, window.dataValues || [], mediaWiki, jQuery );