MediaWiki:Gadget-ImportPagelist.js

/** * Import pagelist gadget * * Adds a link to import a pagelist from a given IA identifier * * Uses the pagelister.toolforge.org tool * * Changelog: *  * 2020-10-24:  Initial version *  * 2020-11-27:  Update cover image from title page, if there is one *  * 2021-01-06:  Handle Hathi links */

/* eslint-disable camelcase

'use strict';

// IIFE used when including as a user script (to allow debug or config) // Default gadget use will get an IIFE wrapper as well ( function {

mw.messages.set( {		'ipl-to-be-proofread-status': 'C',		'ipl-cancel': 'Cancel',		'ipl-import': 'Import',		'ipl-dialog-title': 'Import pagelist',		'ipl-source-ia': 'Internet Archive',		'ipl-source-hathi': 'Hathi Trust',		'ipl-source': 'Source',		'ipl-id': 'ID',		'ipl-offset': 'Offset',		'ipl-offset-help': 'Offset between the source and the file. If the file are identical, this is 0. If a cover page has been removed, it is 1',

'ipl-first-page': 'First page', 'ipl-first-page-title': 'The first page (as an index in the file)', 'ipl-repeat': 'Repeat', 'ipl-repeat-title': 'How many times to repeat the whole pattern', 'ipl-non-numeric-pages': 'Non-numeric pages', 'ipl-non-numeric-pages-title': 'Enter a list of non numeric pages, for example "Img,-,Img,-',		'ipl-non-numeric-default': 'Img,-',		'ipl-non-numeric-not-found-error': 'The line above the cursor does not appear to contain a page number that can be used. E.g. 100=152.',

'ipl-error-msg': 'An error occurred:', 'ipl-bad-response-msg': 'Bad response from pagelist server',

'ipl-link-label': 'Import pagelist', 'ipl-link-title': 'Import pagelist from an external source like the Internet Archive or HathiTrust', 'ipl-nonnum-label': 'Add non-numeric pages', 'ipl-nonnum-title': 'Add non-numeric page entries at the current cursor position.', 'ipl-link-page-game': 'Open in WS Page Game', 'ipl-link-page-game-title': 'Open in the external WS Page Game tool',

'ipl-set-pagelist-summary': 'Importing pagelist using the Import Pagelist gadget (source: $1:$2)', 'ipl-ctrl-enter-to-confirm': ' Ctrl + Enter to confirm.' } );

var gadgetName = 'import_pagelist';

var IPL = { enabled: true, configured: false, host: 'https://pagelister.toolforge.org', pageGameUrl: 'https://ws-page-game.toolforge.org', sharedRepoUrl: 'https://commons.wikimedia.org/w/api.php', setIndexStatus: false };

function getInternetArchiveIdFromUrl( url ) { var rx = /(details|manage|download)\/([^/]*)/, match = rx.exec( url.pathname );

if ( match ) { return match[ 2 ]; }

return null; }

function getHathiIdFromUrl( url ) { if ( url.hostname.match( /hathitrust.org$/ ) ) { // null if not found return url.searchParams.get( 'id' ); } else if ( url.hostname.match( /hdl.handle.net$/ ) ) { return url.pathname.split( '/' )[ 2 ]; }		return null; }

// Find IA links in a page's content function findUsefulLinks( data ) {

for ( var page in data.query.pages ) { var extlinks = data.query.pages[ page ].extlinks;

if ( extlinks ) { for ( var eli = 0; eli < extlinks.length; eli++ ) { var el = extlinks[ eli ].url;

if ( el.startsWith( '//' ) ) { el = window.location.protocol + l;					} var url = new URL( el );

var id; if ( url.hostname.match( /archive.org$/ ) ) { id = getInternetArchiveIdFromUrl( url ); if ( id !== null ) { return { source: 'ia', id: id }; }					} else if ( url.hostname.match( /hathitrust.org$/ ) ||						( url.hostname.match( /hdl.handle.net$/ ) ) ) { id = getHathiIdFromUrl( url ); if ( id !== null ) { return { source: 'ht', id: id }; }					}				}			}

var iwlinks = data.query.pages[ page ].iwlinks;

if ( iwlinks ) { for ( var i = 0; i < iwlinks.length; i++ ) { var l = iwlinks[ i ]; if ( l.prefix === 'iarchive' ) { return { source: 'ia', id: l.title }; }				}			}		}

return null; }

function getIsFileLocal( fn, callback ) { // figure out if the file is local or shared var params = { action: 'query', format: 'json', formatversion: '2', prop: 'imageinfo', iiprop: '', titles: fn		};

new mw.Api.get( params ).done( function ( data ) {			callback( data.query.pages[ 0 ].imagerepository === 'local' );		} ); }

function findLikelyIds( callback ) {

var fn = 'File:' + mw.config.get( 'wgTitle' );

mw.loader.using( [ 'mediawiki.ForeignApi', 'mediawiki.api' ], function {

getIsFileLocal( fn, function ( local ) {

var api = local ? new mw.Api : new mw.ForeignApi( IPL.sharedRepoUrl );

// Get all external links from the Commons file page api.get( {					action: 'query',					format: 'json',					formatversion: 2,					prop: 'extlinks|iwlinks',					redirects: true,					titles: fn,					ellimit: 100,					iwlimit: 100				} ).done( function ( data ) {					var link = findUsefulLinks( data );

if ( link !== null ) { callback( link ); }

} ).fail( function ( data ) { console.log( 'Commons GET Failed:', data ); } );			} );		} );	}

function setPagelistValue( plv ) { OO.ui.infuse( $( '#wpprpindex-Pages' ).parent ).setValue( plv ); }

function setCoverValue( plv ) { // Set the cover image if there is one var match = /(\d+)=["']?[Tt]itle["']\s/.exec( plv );

if ( match ) { $( '#wpprpindex-Image' ).val( match[ 1 ] ); }	}

function setIndexStatus { if ( IPL.setIndexStatus ) { OO.ui.infuse( $( '#wpprpindex-Progress' ).parent ) .setValue( mw.msg( 'ipl-to-be-proofread-status' ) ); }	}

function setPagelistFieldEnabled( enabled ) { OO.ui.infuse( $( '#wpprpindex-Pages' ).parent ).setDisabled( !enabled ); }

function setSummary( source, id ) { $( '#wpSummary' ).val( mw.msg( 'ipl-set-pagelist-summary', source, id ) ); }

function installDialog {

var ParamDialog = function ( config ) { config = config || {}; config.escapable = true; ParamDialog.super.call( this, config ); };		OO.inheritClass( ParamDialog, OO.ui.ProcessDialog );

// Specify a name for .addWindows ParamDialog.static.name = 'importPageListDialog'; ParamDialog.static.title = mw.msg( 'ipl-dialog-title' ); // Specify the static configurations: title and action set ParamDialog.static.actions = [ { flags: 'primary', label: mw.msg( 'ipl-import' ), action: 'open' },		{			flags: 'safe', label: mw.msg( 'ipl-cancel' ) }		];

// Customize the initialize function to add content and layouts: ParamDialog.prototype.initialize = function { ParamDialog.super.prototype.initialize.call( this ); this.panel = new OO.ui.PanelLayout( {				padded: true,				expanded: false			} ); this.content = new OO.ui.FieldsetLayout;

this.inputs = {}; this.fields = {};

this.inputs.source = new OO.ui.DropdownInputWidget( {				options: [					{ data: 'ia', label: mw.msg( 'ipl-source-ia' ) },					{ data: 'ht', label: mw.msg( 'ipl-source-hathi' ) }				] } );

// this.inputs['source'].selectItem( option1 );

this.fields.source = new OO.ui.FieldLayout( this.inputs.source, {				label: mw.msg( 'ipl-source' ),				align: 'right'			} ); this.content.addItems( [ this.fields.source ] );

this.inputs.id = new OO.ui.TextInputWidget;

this.fields.id = new OO.ui.FieldLayout( this.inputs.id, {				label: mw.msg( 'ipl-id' ),				align: 'right'			} ); this.content.addItems( [ this.fields.id ] );

this.inputs.offset = new OO.ui.NumberInputWidget( {				value: 0,				step: 1			} );

this.fields.offset = new OO.ui.FieldLayout( this.inputs.offset, {				label: mw.msg( 'ipl-offset' ),				help: mw.msg( 'ipl-offset-help' ),				align: 'right'			} ); this.content.addItems( [ this.fields.offset ] );

this.panel.$element.append( this.content.$element );

var toolLink = '' + IPL.host.replace( /https?:\/\//, '' ) + '';

this.panel.$element.append( $( ' Note: this tool will access ' + toolLink + '. ' ) );

// disable help tabbing, which gets in the way a LOT this.content.$element.find( '.oo-ui-fieldLayout-help a' ) .attr( 'tabindex', '-1' );

this.$body.append( this.panel.$element ); };

ParamDialog.prototype.prefill_id = function ( link ) { this.inputs.id.setValue( link.id ); this.inputs.source.setValue( link.source ); };

// Use getSetupProcess to set up the window with data passed to it at the time // of opening (e.g., url: 'http://www.mediawiki.org', in this example). ParamDialog.prototype.getSetupProcess = function ( data ) { var self = this; data = data || {}; return ParamDialog.super.prototype.getSetupProcess.call( this, data ) .next( function {					// Fire off a request to see if we can find any useful IDs in the commons page					findLikelyIds( function ( data ) { self.prefill_id( data ); } );				}, this ); };

function report_error( msg ) { alert( mw.msg( 'ipl-error-msg' ) + '\n\n' + msg ); }

// Specify processes to handle the actions. ParamDialog.prototype.getActionProcess = function ( action ) { var dialog = this; if ( action === 'open' ) { // Create a new process to handle the action return new OO.ui.Process( function {					setPagelistFieldEnabled( false );

var source = dialog.inputs.source.getValue; var extId = dialog.inputs.id.getValue;

fetch( IPL.host + '/pagelist/v1/list?' + new URLSearchParams( { source: source, id: extId, offset: dialog.inputs.offset.getNumericValue } ) )						.then( function ( response ) { return response.json; } ) .then( function ( data ) {							if ( data ) {

if ( data.errors !== undefined ) { var errors = data.errors.map( function ( e ) {										return e.msg;									} ).join( '\n' ); report_error( errors ); } else if ( data.pagelist !== undefined ) { var plv = data.pagelist; setPagelistValue( plv ); setCoverValue( plv ); setIndexStatus; setSummary( source, extId ); } else { report_error( mw.msg( 'ipl-bad-response-msg' ) ); }							}						} )						.finally( function { setPagelistFieldEnabled( true ); } );

dialog.close( {						action: action					} ); }, this );			}			// Fallback to parent handler			return ParamDialog.super.prototype.getActionProcess.call( this, action );		};

ParamDialog.prototype.getTeardownProcess = function ( data ) { return ParamDialog.super.prototype.getTeardownProcess.call( this, data ) .first( function {					// Perform any cleanup as needed				}, this ); };

// Create and append a window manager. var windowManager = new OO.ui.WindowManager; $( 'body' ).append( windowManager.$element );

// Create a new process dialog window. var paramDlg = new ParamDialog;

// Add the window to window manager using the addWindows method. windowManager.addWindows( [ paramDlg ] );

// Open the window! windowManager.openWindow( paramDlg );

// focus the input in just a moment setTimeout( function {			paramDlg.$body.find( 'input' )[ 0 ].focus;		}, 300 ); }

// User clicked - install the dialog function activate( evt ) { mw.loader.using( [ 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-widgets' ],			installDialog ); evt.preventDefault; }

function install_non_numeric_dialog( setupData ) {

var ParamDialog = function ( config ) { ParamDialog.super.call( this, config ); };		OO.inheritClass( ParamDialog, OO.ui.ProcessDialog );

// Specify a name for .addWindows ParamDialog.static.name = 'addNonNumericDialog'; ParamDialog.static.title = IPL.nonnum_dialog_title; ParamDialog.static.escapable = true; // Specify the static configurations: title and action set ParamDialog.static.actions = [ { flags: 'primary', label: 'OK', action: 'insert' },		{			flags: 'safe', label: mw.msg( 'ipl-cancel' ) }		];

// Customize the initialize function to add content and layouts: ParamDialog.prototype.initialize = function { ParamDialog.super.prototype.initialize.call( this ); this.panel = new OO.ui.PanelLayout( {				padded: true,				expanded: false,				escapable: true			} ); this.content = new OO.ui.FieldsetLayout;

this.inputs = {}; this.fields = {};

this.inputs.firstIndex = new OO.ui.NumberInputWidget( {				value: 0,				min: 1,				step: 1			} );

this.fields.firstIndex = new OO.ui.FieldLayout(				this.inputs.firstIndex,				{					label: mw.msg( 'ipl-first-page' ),					help: mw.msg( 'ipl-first-page-title' ),					align: 'right'				} );

this.inputs.pages = new OO.ui.TextInputWidget( {				value: 'Img,-'			} ); this.fields.pages = new OO.ui.FieldLayout( this.inputs.pages, {				label: mw.msg( 'ipl-non-numeric-pages' ),				help: mw.msg( 'ipl-non-numeric-pages-title' ),				align: 'right'			} );

this.inputs.repeatCount = new OO.ui.NumberInputWidget( {				value: 1,				min: 0,				step: 1			} );

this.fields.repeatCount = new OO.ui.FieldLayout(				this.inputs.repeatCount,				{					label: mw.msg( 'ipl-repeat' ),					help: mw.msg( 'ipl-repeat-title' ),					align: 'right'				} );

this.content.addItems( [ this.fields.firstIndex, this.fields.pages,				this.fields.repeatCount ] );

this.panel.$element.append( this.content.$element );

this.panel.$element.append(				$( ' ' ),				$( ' ' )					.css( 'font-size', '90%' )					.append( mw.msg( 'ipl-ctrl-enter-to-confirm' ) )			);

// disable help tabbing, which gets in the way a LOT this.content.$element.find( '.oo-ui-fieldLayout-help a' ) .attr( 'tabindex', '-1' );

this.$body.append( this.panel.$element ); };

// Use getSetupProcess to set up the window with data passed to it at the time // of opening (e.g., url: 'http://www.mediawiki.org', in this example). ParamDialog.prototype.getSetupProcess = function ( data ) { var dialog = this; data = data || {}; return ParamDialog.super.prototype.getSetupProcess.call( this, data ) .next( function {					// pre-fill a page number					dialog.inputs.firstIndex.setValue( data.firstIndex );					dialog.saveCallback = data.saveCallback;				}, this ); };

// Specify processes to handle the actions. ParamDialog.prototype.getActionProcess = function ( action ) { var dialog = this; if ( action === 'insert' ) { // Create a new process to handle the action return new OO.ui.Process( function {

var pages = dialog.inputs.pages.getValue.split( ',' ); pages = pages.map( function ( s ) {						s = s.trim;

if ( [ 'img', 'plate', 'cover' ].indexOf( s.toLowerCase ) !== -1 ) { s = s[ 0 ].toUpperCase + s.substring( 1 ).toLowerCase; }

if ( s === '-' ) { s = '–'; }

return s;					} );

var repeated = [];

var rpt = dialog.inputs.repeatCount.getNumericValue; if ( rpt > 0 ) { for ( var i = 0; i < rpt; ++i ) { repeated = repeated.concat( pages ); }					}

var retData = { firstIndex: dialog.inputs.firstIndex.getNumericValue, pages: repeated };

dialog.saveCallback( retData );

dialog.close( {						action: action					} ); }, this );			}			// Fallback to parent handler			return ParamDialog.super.prototype.getActionProcess.call( this, action );		};

// Create and append a window manager. var windowManager = new OO.ui.WindowManager; $( 'body' ).append( windowManager.$element );

// Create a new process dialog window. var paramDlg = new ParamDialog;

// Add the window to window manager using the addWindows method. windowManager.addWindows( [ paramDlg ] );

// Open the window! windowManager.openWindow( paramDlg, setupData );

// focus the input in just a moment setTimeout( function {			paramDlg.$body.find( 'input' )[ 0 ].focus;		}, 500 ); }

function getPreviousLine( val, pos ) { var prevLineEnd = val.lastIndexOf( '\n', pos ); var prevLineStart = val.lastIndexOf( '\n', prevLineEnd - 1 ) + 1;

var prevLine = val.substring( prevLineStart, prevLineEnd );

return prevLine; }

function getPageListParts( line ) { var parts = line.split( '=' );

if ( parts.length === 2 ) { // strip whitespace and quotes return parts.map( function ( s ) {				return s.replace( /\s*['"].*['"]?\s*/g, '' );			} ); }

return null; }

function insertImgDash( evt ) {

mw.loader.using(			[ 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-widgets' ],			function {				var $tb = $( '#wpprpindex-Pages' );

var pos = $tb.getCursorPosition; var val = $tb.val;

// skip up to the last line while ( pos > 0 && val[ pos - 1 ] === '\n' ) { pos--; }

var prevLine = getPreviousLine( val, pos );

var lineParts = getPageListParts( prevLine );

var offset; if ( lineParts ) { offset = parseInt( lineParts[ 1 ] ) - parseInt( lineParts[ 0 ] ); }

if ( offset === undefined || Number.isNaN( offset ) ) { mw.notify( mw.msg( 'ipl-non-numeric-not-found-error' ),						{ autoHide: false, type: 'error' }					); } else { // function to run when the dialog returns var update = function ( params ) { var pageIndex = params.firstIndex;

var newText = '';

var firstVal; var allSame = true;

params.pages.forEach( function ( page ) {							if ( !firstVal ) {								firstVal = page;							}

if ( allSame && page !== firstVal ) { allSame = false; }						} );

if ( allSame && params.pages.length > 1 ) { newText += '\n' + pageIndex + 'to' + ( pageIndex + params.pages.length - 1 ) + '="' + firstVal + '"'; pageIndex += params.pages.length; } else { params.pages.forEach( function ( page ) {								newText += '\n' + pageIndex + '="' + page + '"';								pageIndex++;							} ); }

newText += '\n' + pageIndex + '=' + ( params.firstIndex + offset ); newText += '\n\n'; val = val.substring( 0, pos ) + newText + val.substring( pos ).trimStart; $tb.val( val );

$tb.setCursorPosition( pos + newText.length - 1 );

setTimeout( function {							$tb.trigger( 'focus' );						}, 500 ); };

install_non_numeric_dialog( {						saveCallback: update,						firstIndex: parseInt( lineParts[ 0 ] ) + 1					} ); }			}		);

evt.preventDefault; }

function goToPageGameUrl { var url = IPL.pageGameUrl + '?index=' + mw.config.get( 'wgTitle' ) + '&wikisource=' + mw.config.get( 'wgWikiID' );

return url; }

function makeButton( label, title, options ) { var $link = $( '' ) .attr( 'href', options.href || '#' ) .append( label ) .attr( 'title', title ) .addClass( 'popups_nopopup' );

if ( options.click ) { $link.on( 'click', options.click ); }

return $link; }

// Insert the button next to the right field function insertButtons { var $importBtn = makeButton( mw.msg( 'ipl-link-label' ),			mw.msg( 'ipl-link-title' ), {				click: activate			} ); var $insertImgDash = makeButton( mw.msg( 'ipl-nonnum-label' ),			mw.msg( 'ipl-nonnum-title' ), {				click: insertImgDash			} ); var $goToPageGame = makeButton( mw.msg( 'ipl-link-page-game' ),			mw.msg( 'ipl-link-page-game-title' ), {				href: goToPageGameUrl			} );

var $list = $( '' ) .addClass( 'gadgetjs-ipl-toollist' );

[ $importBtn, $insertImgDash, $goToPageGame ].forEach( function ( $newBtn ) {			$( '' )				.append( $newBtn )				.appendTo( $list );		} );

// eslint-disable-next-line no-jquery/no-global-selector $( '#wpprpindex-Pages' ) .closest( '.oo-ui-fieldLayout-body' ) .children( '.oo-ui-fieldLayout-header' ) .find( 'label' ) .append( $list ); }

function addCssRule( css ) { $( ' ' ).prop( 'type', 'text/css' ).html( css ).appendTo( 'head' ); }

function iplSetup {

// Get user config, if any mw.hook( gadgetName + '.config' ).fire( IPL );

IPL.configured = true;

// only care for editing in the Index: namespace if ( !( mw.config.get( 'wgAction' ) === 'edit' || mw.config.get( 'wgAction' ) === 'submit' ) ||		mw.config.get( 'wgCanonicalNamespace' ) !== 'Index' ) { return; }

// eslint-disable-next-line no-multi-str var css = '\ .gadgetjs-ipl-toollist { \ list-style: none; \ font-size: 92%; \ float: right; \ text-align: right; \ } \ ';		addCssRule( css );

insertButtons; }

( function ( $ ) {		$.fn.getCursorPosition = function {			var el = $( this ).get( 0 );			var pos = 0;			if ( 'selectionStart' in el ) {				pos = el.selectionStart;			} else if ( 'selection' in document ) {				el.focus;				var Sel = document.selection.createRange;				var SelLength = document.selection.createRange.text.length;				Sel.moveStart( 'character', -el.value.length );				pos = Sel.text.length - SelLength;			}			return pos;		};

$.fn.setCursorPosition = function ( start, end ) { if ( end === undefined ) { end = start; }			return this.each( function {				if ( 'selectionStart' in this ) {					this.selectionStart = start;					this.selectionEnd = end;				} else if ( this.setSelectionRange ) {					this.setSelectionRange( start, end );				} else if ( this.createTextRange ) {					var range = this.createTextRange;					range.collapse( true );					range.moveEnd( 'character', end );					range.moveStart( 'character', start );					range.select;				}			} ); };	}( $ ) );

$( iplSetup );

} );