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.
/**
* 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 [[MediaWiki:Gadget-ImportPagelist|Import Pagelist gadget]] (source: $1:$2)',
'ipl-ctrl-enter-to-confirm': '<kbd>Ctrl</kbd>+<kbd>Enter</kbd> 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 = '<a href="' + IPL.host + '">' + IPL.host.replace( /https?:\/\//, '' ) + '</a>';
this.panel.$element.append( $( '<hr/><p style="font-size:90%;">Note: this tool will access ' + toolLink + '.</p>' ) );
// 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(
$( '<hr>' ),
$( '<p>' )
.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 = $( '<a>' )
.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 = $( '<ul>' )
.addClass( 'gadgetjs-ipl-toollist' );
[ $importBtn, $insertImgDash, $goToPageGame ].forEach( function ( $newBtn ) {
$( '<li>' )
.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 ) {
$( '<style>' ).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 );
}() );