MediaWiki:Wp/isv/Common.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.
/*
* Medžuviki transliterator
* Converts visible content into Latin or Cyrillic
* by [[User:Lev]] (originally at https://isv.miraheze.org/wiki/MediaWiki:Gadget-alphabet.js)
*
* Borrows from: https://www.mediawiki.org/wiki/MediaWiki:Gadget-Numerakri.js
* @license <https://opensource.org/licenses/MIT>
*/
(function () {
// MediaWiki config options
var config = mw.config.get( [
'wgAction',
'wgContentLanguage',
'wgNamespaceNumber',
'wgPageContentModel',
'wgUserName',
'skin'
] );
if (
// Only when viewing content
config.wgAction !== 'view'
// Any non-content pages
|| config.wgPageContentModel !== 'wikitext'
// Special, MediaWiki pages
|| [ -1, 8 ].indexOf( config.wgNamespaceNumber ) > -1
) return;
// Variant indexes
var varIndex = {
default: 0,
latn: 0,
cyrl: 1
};
// Re-used variables
var walker = null;
var api;
var targetStyle;
var currentStyle = 'default';
var defaultStorageKey = '__defaultAlphabetText';
var ignoreClass = 'ext-gadget-alphabet-disable';
var romanNumerals = [];
// HTML tags that should not be touched by parser
var skippedTags = [
'code',
'input',
'link',
'kbd',
'noscript',
'pre',
'style',
'textarea'
];
// Standard label text
var varLabels = {
'default': 'Lat./Кир.',
'latn': 'Latinica',
'cyrl': 'Кирилица'
};
/**
* Replacements (for Latin as a default).
* Syntax: ["Latin", "Cyrillic"],
* Put additional conversions of same letters after the main one.
*/
var data = {
outliers: [
// Use acutes for disambiguation purposes in Cyrillic
[ 'lj', 'љ' ],
// [ 'ĺj', 'лј' ],
[ 'nj', 'њ' ],
// [ 'ńj', 'нј' ]
],
mappings: [
[ 'a', 'а' ],
[ 'b', 'б' ],
[ 'c', 'ц' ],
[ 'č', 'ч' ],
[ 'd', 'д' ],
[ 'e', 'е' ],
[ 'ě', 'є' ],
[ 'f', 'ф' ],
[ 'g', 'г' ],
[ 'h', 'х' ],
[ 'i', 'и' ],
[ 'j', 'ј' ],
[ 'k', 'к' ],
[ 'l', 'л' ],
[ 'ĺ', 'л' ],
[ 'm', 'м' ],
[ 'n', 'н' ],
[ 'ń', 'н' ],
[ 'o', 'о' ],
[ 'p', 'п' ],
[ 'r', 'р' ],
[ 's', 'с' ],
[ 'š', 'ш' ],
[ 't', 'т' ],
[ 'u', 'у' ],
[ 'v', 'в' ],
[ 'y', 'ы' ],
[ 'z', 'з' ],
[ 'ž', 'ж' ],
// Optional (Extended) Interslavic, do not use in text preferably
[ 'å', 'а' ],
[ 'ć', 'ч' ],
[ 'ď', 'д' ],
[ 'đ', 'дж' ],
[ 'ė', 'е' ],
[ 'ę', 'е' ],
[ 'ľ', 'љ' ],
[ 'ň', 'н' ],
[ 'ò', 'о' ],
[ 'ŕ', 'р' ],
[ 'ř', 'р' ],
[ 'ś', 'с' ],
[ 'ť', 'т' ],
[ 'ų', 'у' ],
[ 'ź', 'з' ],
// Solely for Cyrillic to Latin conversions
[ 'šč', 'щ' ],
[ 'j', 'ь' ],
[ 'ja', 'я' ],
[ 'ju', 'ю' ],
[ 'e', 'э' ],
[ 'e', 'ѣ' ],
// Non-used letters that can be good to have converted
[ 'w', 'в' ],
[ 'ł', 'л' ],
[ 'ö', 'ӧ' ],
[ 'ü', 'ӱ' ],
[ 'x', 'кс' ]
]
};
/**
* Filter the text nodes for tree walker.
*
* @param {HTMLElement|TextNode} node
* @return {number} NodeFilter.FILTER_* constant
*/
function filterNode( node ) {
if ( node.nodeType === Node.TEXT_NODE ) {
// Skip whitespace
if ( !/\S/.test( node.nodeValue ) ) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
// Skip this element and skip its children
var tag = node.nodeName && node.nodeName.toLowerCase();
if ( skippedTags.indexOf( tag ) > -1 ) return NodeFilter.FILTER_REJECT;
var lang = $( node ).attr( 'lang' );
var hasSkipClass = $( node ).hasClass( ignoreClass );
if ( /*( lang && lang !== config.wgContentLanguage ) ||*/ hasSkipClass ) {
return NodeFilter.FILTER_REJECT;
}
// Skip this element, but check its children
return NodeFilter.FILTER_SKIP;
}
/**
* Replace all text in the filtered nodes.
*
* @param {TextNode} node
*/
function handleTextNode( node ) {
if ( targetStyle === 'default' ) {
restoreDefaults( node );
return;
}
var original = node.nodeValue;
var changed = original;
changed = removeRomanNumerals( changed );
changed = replaceText( changed );
changed = fixRomanNumerals( changed );
storeDefaultValue( node, original, changed );
if ( original !== changed ) {
node.nodeValue = changed;
}
}
/**
* Restore defaults in all nodes (if possible).
*
* @param {TextNode} node
*/
function restoreDefaults( node ) {
var defaults = node.parentNode[ defaultStorageKey ];
var value = node.nodeValue;
if ( typeof defaults !== 'object' || defaults === null ) {
return;
}
if ( defaults[ value ] !== '' ) {
node.nodeValue = defaults[ value ];
}
}
/**
* Set defaults in the parent node.
*
* @param {TextNode} node
* @param {string} original
* @param {string} changed
*/
function storeDefaultValue( node, original, changed ) {
var parent = node.parentNode;
if ( typeof parent[ defaultStorageKey ] !== 'object' || parent[ defaultStorageKey ] === null ) {
parent[ defaultStorageKey ] = {};
}
if ( currentStyle === 'default' ) {
parent[ defaultStorageKey ][ changed ] = original;
return;
}
// Get default value from previous conversion
if ( original in parent[ defaultStorageKey ] ) {
parent[ defaultStorageKey ][ changed ] = parent[ defaultStorageKey ][ original ];
}
}
/**
* Handling function for requestIdleCallback.
* See https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw-method-requestIdleCallback
*/
function idleWalker( deadline ) {
var el;
if ( !walker ) {
return;
}
while ( deadline.timeRemaining() > 0 ) {
el = walker.nextNode();
if ( !el ) {
// Reached the end
walker = null;
currentStyle = targetStyle;
targetStyle = null;
return;
}
handleTextNode( el );
}
// The user may interact with the page. We pause so the browser can process
// interaction. The text handler will continue after that.
if ( walker ) {
mw.requestIdleCallback( idleWalker );
}
}
/**
* Transliterate the content into one of the options.
*
* @param event The event or object: outputStyle ("cyrl", "latn"), trigger.
*/
function startPageConversion( event ) {
if ( event.trigger !== true ) {
event.preventDefault();
}
targetStyle = event.data.outputStyle;
// Nothing to change, just show the default page
if ( event.trigger !== true && currentStyle === targetStyle ) {
return;
}
// Change selected tab and save variant
var $targetTab = $( '#ca-varlang-' + targetStyle );
$( '[id^="ca-varlang"].selected' ).removeClass( 'selected' );
$( $targetTab ).addClass( 'selected' );
$( '#p-variants-label > span' ).text( $targetTab.text() );
if ( event.trigger !== true ) {
setVariant( targetStyle );
}
if ( event.trigger === true && targetStyle === 'default' ) {
return;
}
// If a walker is already active, replace it.
// If no walker is active yet, start it.
if ( !walker ) {
mw.requestIdleCallback( idleWalker );
}
walker = document.createTreeWalker(
document.querySelector( '#mw-content-text' ),
NodeFilter.SHOW_ALL,
filterNode,
false
);
// Change interface language
var lang = config.wgContentLanguage + ( targetStyle !== 'default' ? '-' + targetStyle : '' );
document.querySelector( '#mw-content-text' ).setAttribute( 'lang', lang );
document.documentElement.setAttribute( 'data-variant', lang );
}
/**
* Replace occurrences of a letter sequence
*
* @param {TextNode} str Text to do replacements in.
* @param {Array} data Replacement data.
* @param style Possible values: "default", "latn", "cyrl".
* @return {TextNode} Text with replacements.
*/
function replaceSequence( str, data, style ) {
var input = data[ + !varIndex[ style ] ];
var output = data[ varIndex[ style ] ];
// Small function for uppercasing first letter only
function capitalize( string ) {
return string.charAt( 0 ).toUpperCase() + string.slice( 1 );
}
var capInput = capitalize( input );
var capOutput = capitalize( output );
var uppInput = input.toUpperCase();
if ( !String.prototype.replaceAll ) {
return str.replace( new RegExp( input, 'g' ), output )
.replace( new RegExp( capInput, g ), capOutput )
.replace( new RegExp( input.toUpperCase(), g ), capOutput );
}
return str.replaceAll( input, output )
.replaceAll( capInput, capOutput )
.replaceAll( uppInput, capOutput );
}
/**
* Replace text
*
* @param {TextNode} str Text to do replacements in.
* @param style Possible values: "default", "latn", "cyrl".
*/
function replaceText( str, style ) {
style = style || targetStyle;
// Replace outliers first
for ( var i = 0; i < data.outliers.length; i++ ) {
str = replaceSequence( str, data.outliers[ i ], style );
}
// Replace the letters
for ( var i = 0; i < data.mappings.length; i++ ) {
str = replaceSequence( str, data.mappings[ i ], style );
}
return str;
}
/**
* Remove Roman numerals from the script
* See https://phabricator.wikimedia.org/source/mediawiki/browse/master/includes/language/converters/ShConverter.php$132
*
* @param {TextNode} str Text to do replacements in.
*/
function removeRomanNumerals( str ) {
romanNumerals = [];
var romanRegex = /\b(?=[MDCLXVI])M{0,4}(C[DM]|D?C{0,3})(X[LC]|L?X{0,3})(I[VX]|V?I{0,3})\b/g;
return str.replace( romanRegex, function( match, $1 ) {
if ( match.length <= 1 ) {
return match;
}
var count = romanNumerals.length;
// Also in fixRomanNumerals()
var id = '----' + count + '----';
romanNumerals.push( match );
return id;
} );
}
/**
* Fix Roman numerals previously removed by the script
*
* @param {TextNode} str Text to do replacements in.
*/
function fixRomanNumerals( str ) {
var idRegex = /----([\d+])----/g;
return str.replace( idRegex, function( match, $1 ) {
var numeral = romanNumerals[ $1 ];
return numeral !== null ? numeral : match;
} );
}
/**
* Read user option / local storage for variant.
*
* @return {string} Value from option / local storage or "default".
*/
function getVariant() {
var value;
if ( config.wgUserName === null ) {
mw.loader.using( 'mediawiki.storage' ).done( function() {
value = mw.storage.get( 'ext-gadget-alphabet' );
value = ( value !== null ? value : 'default' );
} );
return value;
}
mw.loader.using( 'mediawiki.user' ).done( function() {
value = mw.user.options.get( 'userjs-ext-gadget-alphabet' );
value = ( value !== null ? value : 'default' );
} );
return value;
}
/**
* Set user option / cookie for variant.
*
* @param name Possible values: "default", "latn", "cyrl".
*/
function setVariant( name, prev ) {
var isDefault = ( name === 'default' );
var message = 'Vaše prědpočitańje azbuky bylo ' + ( isDefault ? 'udaljeno.' : 'zapisano.' );
if ( currentStyle === 'cyrl' || name === 'cyrl' ) {
message = replaceText( message, 'cyrl' );
}
if ( config.wgUserName === null ) {
mw.loader.using( 'mediawiki.storage' ).done( function() {
var action = ( isDefault ? 'remove' : 'set' );
var stored = mw.storage[ action ]( 'ext-gadget-alphabet', name );
if ( stored ) mw.notify( message );
} );
return;
}
mw.loader.using( [ 'mediawiki.api', 'mediawiki.user' ] ).done( function() {
if ( !api ) api = new mw.Api();
var value = ( isDefault ? null : name );
api.saveOption( 'userjs-ext-gadget-alphabet', value ).then( function() {
mw.notify( message );
} );
} );
}
/**
* Add alphabet variants and start initial conversion.
*/
function init() {
var isVector = ( config.skin === 'vector' || config.skin === 'vector-2022' );
var isMinerva = config.skin === 'minerva';
if ( isVector ) {
$( '#p-variants' ).removeClass( 'emptyPortlet' );
$( '#p-variants-label' ).addClass( ignoreClass );
}
if ( isMinerva ) {
// Hacky way to add tabs in Minerva
var $wrapper = $( '<div style="float:right;">' );
var $minervaPortletLink = $( 'a[rel="discussion"]' ).clone()
.attr( 'href', '/wiki/Wp/isv/Vikipedija:Pomoč/Transliteracija' )
.removeClass( 'new' )
.removeAttr( 'rel' ).removeAttr( 'data-event-name' );
$( '.minerva__tab-container' ).append( $wrapper );
}
function addPortletLink( code ) {
var $link;
if ( isMinerva ) {
$link = $minervaPortletLink.clone()
.attr( 'id', 'ca-varlang-' + code )
.text( varLabels[ code ] );
if ( code === 'default' ) $link.addClass( 'selected' );
$wrapper.append( $link );
} else {
var $portlet = $( mw.util.addPortletLink(
( isVector ? 'p-variants' : 'p-cactions' ),
'/wiki/Wp/isv/Vikipedija:Pomoč/Transliteracija',
varLabels[ code ],
'ca-varlang-' + code
) );
var lang = config.wgContentLanguage + ( code !== 'default' ? '-' + code : '' );
if ( code === 'default' ) $portlet.addClass( 'selected' );
$portlet.attr( 'lang', lang ).addClass( ignoreClass );
$link = $portlet.find( 'a' );
}
mw.util.showPortlet( 'vector-variants-dropdown' ); // Fix for Vector-2022
$link.click( { 'outputStyle': code }, startPageConversion );
}
mw.loader.using( 'mediawiki.util' ).done( function() {
addPortletLink( 'default' );
addPortletLink( 'latn' );
addPortletLink( 'cyrl' );
} );
// Start conversion when the document is idle
var variant = getVariant();
$( '#p-variants-label > span' ).text( varLabels[ variant ] );
mw.requestIdleCallback( function() {
startPageConversion( {
data: {
outputStyle: variant
},
trigger: true
} );
} );
}
$( init );
}() );