User:Jon Harald Søby/copySenses.js
Jump to navigation
Jump to search
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.
/**
* Script to copy senses from one lexeme to another.
*
* If the lexeme that is being copied from is in the same language as the
* lexeme you are on, the senses will be added as synonyms of each other.
* If the lexeme is in a different language, the senses will be added as
* translations of each other.
*
* You can copy all senses from one lexeme to another by using just the
* lexeme ID (e.g. L12345), or you can copy just a single sense by using
* the sense ID (e.g. L12345-S1).
*
* If you don't want to add translation statements when using this script,
* add the following line before you include the script:
* mw.config.set( 'userjs-copysenses-excludetranslations', true );
*
* @version 1.6.1 (2023-04-02)
* @author Jon Harald Søby
*/
/* jshint esversion: 8 */
mw.loader.using( [ 'mediawiki.api', 'mediawiki.util', 'oojs', 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows' ] ).then( function() {
'use strict';
if ( mw.config.get( 'wgNamespaceNumber' ) !== 146 ) return;
if ( window.copysenses_excludetranslations ) {
mw.config.set( 'userjs-copysenses-excludetranslations', true );
mw.log.warn(
'copySenses.js: The method `window.copysenses_excludetranslations` is deprecated, ' +
'please use `mw.config.set( \'userjs-copysenses-excludetranslations\', true );` instead.'
);
}
const api = new mw.Api(),
thisscript = 'User:Jon Harald Søby/copySenses.js',
addtranslations = !mw.config.get( 'userjs-copysenses-excludetranslations' ),
version = '1.6.0',
editGroupHash = Math.floor( Math.random() * Math.pow( 2, 48 ) ).toString( 16 ),
summarydetails = ' ([[:toolforge:editgroups/b/copysenses/' + editGroupHash + '|details]])';
console.log( 'copySenses.js v' + version );
let propconf = {
safe: [
'P5137', // item for this sense
'P9970', // predicate for
'P6271', // demonym for
'P18' // image
],
safe_samelang: [
'P5974' // antonym
]
};
let cachedSynonyms = {
'lexemes': new Set()
};
let cachedBacklinks = {};
/**
* Get the next sense ID from the API.
*
* @returns {string}
*/
async function lookupNextSenseId() {
let apires = await api.get( {
action: 'query',
format: 'json',
prop: 'revisions',
titles: mw.config.get( 'wgPageName' ),
formatversion: 2,
rvprop: 'content',
rvslots: 'main'
} ).then( function( data ) {
return JSON.parse( data.query.pages[ 0 ].revisions[ 0 ].slots.main.content );
} );
return apires.nextSenseId;
}
/**
* Get data from another lexeme
*
* @param {string} lexid - Lexeme ID of another lexeme
* @returns {Promise}
*/
async function getOtherLexeme( lexid, singlesense, nextSenseId ) {
if ( singlesense ) {
lexid = lexid.split( '-' )[ 0 ];
}
cachedSynonyms.lexemes.add( lexid );
let apires = await api.get( {
action: 'wbgetentities',
format: 'json',
formatversion: 2,
ids: lexid
} ).then( function( data ) {
if (
data.hasOwnProperty( 'entities' ) &&
data.entities.hasOwnProperty( lexid ) &&
!data.entities[ lexid ].hasOwnProperty( 'missing' ) &&
data.entities[ lexid ].senses.length
) {
let senselist = data.entities[ lexid ].senses,
senseid = nextSenseId - 1;
for ( let sense in senselist ) {
if ( singlesense && senselist[ sense ].id !== singlesense ) {
continue;
}
++senseid;
cachedSynonyms[ 'S' + senseid ] = [ [ lexid, senselist[ sense ].id ] ];
for ( let claim in senselist[ sense ].claims ) {
if ( claim === 'P5972' || claim === 'P5973' ) { // translation || synonym
for ( let statement of senselist[ sense ].claims[ claim ] ) {
let transsense = statement.mainsnak.datavalue.value.id,
translex = transsense.split( '-' )[ 0 ];
cachedSynonyms.lexemes.add( translex );
cachedSynonyms[ 'S' + senseid ].push( [ translex, transsense ] );
}
}
}
}
return data.entities[ lexid ];
} else {
return false;
}
});
return apires;
}
/**
* Add senses from another lexeme to the current lexeme.
*
* @param {string} sourcelexeme - a lexeme ID
* @param {string} thislang -
* @returns {Object[]}
*/
async function prepareSensesForCurrentLexeme( otherlexeme, thislang, singlesense, nextSenseId ) {
let otherLex = JSON.parse( JSON.stringify( otherlexeme ) ),
otherlang = otherLex.language,
samelang = thislang === otherlang,
senses = otherLex.senses,
langcheck = await checkTranslationLanguages( cachedSynonyms, thislang ),
newsenses = [],
senseid = nextSenseId - 1;
for ( let sense of senses ) {
if ( singlesense && singlesense !== sense.id ) {
continue;
}
++senseid;
cachedBacklinks[ 'S' + senseid ] = [];
let newsense = {
add: '',
glosses: sense.glosses,
claims: {}
};
for ( let claim in sense.claims ) {
if ( !newsense.claims.hasOwnProperty( claim ) ) {
newsense.claims[ claim ] = [];
}
for ( let statement of sense.claims[ claim ] ) {
if ( propconf.safe.includes( claim ) ) {
newsense.claims[ claim ].push( statement );
} else if ( samelang && propconf.safe_samelang.includes( claim ) ) {
newsense.claims[ claim ].push( statement );
}
}
}
for ( let claim in newsense.claims ) {
for ( let statement of newsense.claims[ claim ] ) {
delete statement.mainsnak.hash;
delete statement.id;
}
}
for ( let synonym of cachedSynonyms[ 'S' + senseid ] ) {
let samelangcheck = langcheck.samelang.includes( synonym[ 0 ] ),
synprop = samelangcheck ? 'P5973' : 'P5972'; // synonym : translation
if ( !newsense.claims.hasOwnProperty( synprop ) ) {
newsense.claims[ synprop ] = [];
}
if ( samelangcheck || addtranslations ) {
newsense.claims[ synprop ].push( {
mainsnak: {
snaktype: 'value',
property: synprop,
datavalue: {
value: {
'entity-type': 'sense',
id: synonym[ 1 ]
},
type: 'wikibase-entityid'
},
datatype: 'wikibase-sense'
},
type: 'statement',
rank: 'normal'
} );
cachedBacklinks[ 'S' + senseid ].push( { prop: synprop, id: synonym[ 1 ] } );
}
}
newsenses.push( newsense );
}
return newsenses;
}
/**
* Add appropriate statements linking to the current lexeme from the source
* lexeme or any other relevant lexemes (such as other synonyms, for
* instance).
*
* @param {string} otherlexeme - the lexeme ID that should be edited
* @param {string} thislang - QID of this lexeme's language
* @returns {Object[]}
*/
async function addBacklinksToOtherLexeme( otherlexeme, thislang, thisid, singlesense, nextSenseId ) {
let otherLex = JSON.parse( JSON.stringify( otherlexeme ) ),
otherlang = otherLex.language,
prop = ( thislang === otherlang ) ? 'P5973' : 'P5972',
senseid = nextSenseId - 1;
if ( addtranslations || thislang === otherlang ) {
for ( let sense in otherLex.senses ) {
if ( singlesense && singlesense !== otherLex.senses[ sense ].id ) {
continue;
}
++senseid;
let snak = {
add: '',
mainsnak: {
datatype: 'wikibase-sense',
datavalue: {
type: 'wikibase-entityid',
value: {
'entity-type': 'sense',
id: thisid + '-S' + senseid
}
},
property: prop,
snaktype: 'value'
},
rank: 'normal',
type: 'statement'
};
if ( otherLex.senses[ sense ].claims.hasOwnProperty( prop ) ) {
otherLex.senses[ sense ].claims[ prop ].push( snak );
} else {
otherLex.senses[ sense ].claims[ prop ] = [ snak ];
}
}
}
return otherLex.senses;
}
/**
* Check the language of the senses linked to in translation (P5972)
* statements.
*
* @param {Object} cache
* @param {string} thislang - the QID for the lanugage of the current lexeme
* @returns {Object}
*/
async function checkTranslationLanguages( cache, thislang ) {
let samelang = [],
difflang = [];
let apires = await api.get( {
action: 'wbgetentities',
format: 'json',
ids: [...cachedSynonyms.lexemes].join( '|' ),
formatversion: 2
} );
if ( apires.entities ) {
for ( let entity in apires.entities ) {
if ( apires.entities[ entity ].language === thislang ) {
samelang.push( entity );
} else {
difflang.push( entity );
}
}
}
return {
samelang: samelang,
difflang: difflang
};
}
/**
* Save senses to a lexeme
*
* @param {string} lexid - the lexeme to be edited
* @param {string} summary - summary to be used for the edit
* @param {Object} senses
* @returns {Promise}
*/
async function saveSenses( lexid, summary, senses ) {
let apireq = await api.postWithEditToken( {
action: 'wbeditentity',
format: 'json',
formatversion: 2,
id: lexid,
data: JSON.stringify( { senses: senses } ),
summary: summary + summarydetails
} );
return apireq;
}
/**
* Save a single external sense claim with a link back to this lexeme.
*
* @param {string} senseid - the sense ID for the sense on this lexeme
* @param {Object} claim
* @returns {Promise}
*/
async function saveSingleClaim( senseid, claim ) {
let apireq = api.postWithEditToken( {
action: 'wbcreateclaim',
format: 'json',
entity: claim.id,
snaktype: 'value',
property: claim.prop,
value: JSON.stringify( { 'entity-type': 'sense', id: senseid } ),
summary: '[[' + thisscript + '|copySenses.js]]' + summarydetails
});
return apireq;
}
/**
* Turn the API response with the other lexeme into
* human-friendly HTML.
*
* @param {Object} otherlexeme
* @returns {jQuery}
*/
function htmlizeSenses( otherlexeme, singlesense ) {
let thisdiv = $( '<div />' );
if ( !otherlexeme ) {
thisdiv.html( '<i>Not a valid lexeme, or no senses in lexeme.</i>' );
return thisdiv;
}
let senses = otherlexeme.senses,
senseid = 0;
for ( let sense of senses ) {
++senseid;
if ( singlesense && singlesense.split( '-' )[ 1 ] !== 'S' + senseid ) continue;
let sensediv = $( '<div />' ),
sensestrong = $( '<strong>S' + senseid + '</strong>' ),
senseul = $( '<ul>' );
for ( let gloss in sense.glosses ) {
senseul.append( $( '<li><code>' + gloss + '</code>: ' + sense.glosses[ gloss ].value + '</li>' ) );
}
sensediv.append( sensestrong ).append( senseul );
thisdiv.append( sensediv );
}
return thisdiv;
}
mw.hook( 'wikibase.entityPage.entityLoaded' ).add( function( e ) {
const language = e.language;
mw.util.addCSS(
'.wikibase-lexeme-senses { clear: both; } ' +
'.copySenseEntry { width: 20em; } ' +
'#copysensediv { border: 1px solid rgba(128,128,128,0.5); padding: 1em; background-color: rgba(255,255,255,0.3); border-radius: 1em; box-shadow: 0 0 50px #ccc; }'
);
let copySensesTextinput = new OO.ui.TextInputWidget( {
accessKey: ','
} ).on( 'enter', function() {
copySensesButton.$element.click();
} ),
copySensesButton = new OO.ui.ButtonWidget( {
label: 'Copy senses',
flags: [ 'primary', 'progressive' ]
} ),
copySensesField = new OO.ui.ActionFieldLayout( copySensesTextinput, copySensesButton, {
classes: [ 'copySenseEntry' ]
} ),
confirmButton = new OO.ui.ButtonWidget( {
label: 'Confirm',
flags: [ 'primary', 'progressive' ],
accessKey: 's'
}),
cancelButton = new OO.ui.ButtonWidget( {
label: 'Cancel',
flags: [ 'destructive' ],
framed: false
}),
refreshButton = new OO.ui.ButtonWidget( {
label: 'Refresh the page',
flags: [ 'primary', 'progressive' ],
accessKey: 's'
}),
buttonLayout = new OO.ui.HorizontalLayout( {
items: [ confirmButton, cancelButton ]
});
copySensesButton.on( 'click', async function() {
let sourcelexeme = copySensesField.getField().value.toUpperCase().trim(),
nextSenseId = await lookupNextSenseId(),
singlesense = /^L\d+-S\d+$/.test( sourcelexeme ) ? sourcelexeme : false,
otherlexeme = await getOtherLexeme( sourcelexeme, singlesense, nextSenseId ),
addsenseshere = await prepareSensesForCurrentLexeme( otherlexeme, language, singlesense, nextSenseId ),
addbacklinksthere = await addBacklinksToOtherLexeme( otherlexeme, language, e.id, singlesense, nextSenseId );
copySensesField.$element.hide();
$( '#copysensediv' )
.append( htmlizeSenses( otherlexeme, singlesense ) )
.append( buttonLayout.$element.css( 'margin-top', '1em' ) )
.append( $( '<ul id="resultsdiv" />' ) );
confirmButton.on( 'click', async function() {
mw.hook( 'userjs.copysenses' ).fire();
confirmButton.setDisabled( true );
let spinner1 = $.createSpinner( 'spinner1' ),
spinner2 = $.createSpinner( 'spinner2' );
let action1 = $( '<li> Saving senses to this lexeme</li>' ).prepend( spinner1 ),
action2 = $( '<li> Saving links back to this lexeme to ' + sourcelexeme + '</li>' ).prepend( spinner2 );
const editGroupUrl = 'https://editgroups.toolforge.org/b/copysenses/' + editGroupHash + '/';
const $editGroupLink = $( '<div>' )
.html( 'Change your mind? You can revert these changes with <a href="' + editGroupUrl + '">Edit Groups</a>.' );
$( '#resultsdiv' ).append( action1 );
$( '#resultsdiv' ).append( action2 );
let savethis = await saveSenses( e.id, '[[' + thisscript + '|Copy senses]] from [[Lexeme:' + sourcelexeme.split( '-' )[ 0 ] + '|' + sourcelexeme + ']]', addsenseshere );
if ( savethis.hasOwnProperty( 'success' ) ) {
$( '#mw-spinner-spinner1' ).replaceWith( '✔ ' );
let savethat = await saveSenses( sourcelexeme.split( '-' )[ 0 ], 'This lexeme\'s [[' + thisscript + '|senses copied]] to [[Lexeme:' + e.id + '|' + e.id + ']]; adding backlinks', addbacklinksthere );
if ( savethat.hasOwnProperty( 'success' ) ) {
$( '#mw-spinner-spinner2' ).replaceWith( '✔' );
} else {
$( '#mw-spinner-spinner2' ).replaceWith( '❌ ' );
action2.append( $( ' <code class="error">' + savethat.error.code + '</code>' ) );
}
} else {
$( '#mw-spinner-spinner1' ).replaceWith( '❌ ' );
action1.append( $( ' <code class="error">' + savethis.error.code + '</code>' ) );
}
for ( let sense in cachedBacklinks ) {
let senseid = e.id + '-' + sense;
for ( let claim of cachedBacklinks[ sense ] ) {
if ( claim.id.split( '-' )[ 0 ] === sourcelexeme.split( '-' )[ 0 ] ) {
continue;
}
await new Promise( r => setTimeout( r, 250 ) );
let partaction = $( '<li> Saving backlink to ' + sense + ' to ' + claim.id + '</li>' ).prepend( $.createSpinner( claim.id ) );
$( '#resultsdiv' ).append( partaction );
saveSingleClaim( senseid, claim ).then( function( data ) {
if ( data.hasOwnProperty( 'success' ) ) {
$( '#mw-spinner-' + claim.id ).replaceWith( '✔ ' );
} else {
$( '#mw-spinner-' + claim.id ).replaceWith( '❌ ' );
partaction.append( $( '<code class="error">' + data.error.code + '</code>' ) );
}
}).catch( function( err ) {
$( '#mw-spinner-' + claim.id ).replaceWith( '❌ ' );
partaction.append( $( '<code class="error">' + err + '</code>' ) );
});
}
}
refreshButton.on( 'click', function() {
location.reload();
});
$( '#resultsdiv' ).after( refreshButton.$element );
refreshButton.$element.after( $editGroupLink );
});
cancelButton.on( 'click', function() {
location.reload();
});
});
$( '.wikibase-lexeme-senses' ).append( $( '<div id="copysensediv" />') );
$( '#copysensediv' ).html( copySensesField.$element );
});
});