MediaWiki:Gadget-minipedia.js
Revision as of 15:54, 14 August 2020 by Valerio Bozzolan (talk | contribs) (fix linear understanding degradation function)
Note: After saving, 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: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
/**
* Make Minipedia magics
*
* Dependencies: mediawiki.util
*
* See https://phabricator.wikimedia.org/tag/minipedia/
*
* @revision 2020-06-27
*/
( function ( $, mw ) {
/*
* CONFIGURATION/LOCALIZATION INSTRUCTIONS
*
* Declare somewhere something like this:
*
* // assure that you do not overwrite other-people customizations
* window.MiniPedia = window.MiniPedia || {};
* window.MiniPedia.L10N = window.MiniPedia.L10N || {};
*
* // then customize something
* window.MiniPedia.editIntro = 'Project:How to create';
* window.MiniPedia.L10N.minipedia = 'Otherpedia';
*/
// load localization defaults
var DEFAULTS = {
// namespace prefix configured in your LocalSettings.php
namespace: 'Mini',
// namespace number configured in your LocalSettings.php
namespaceNum: 3002,
// default edit intro page title
editIntro: 'Progetto:Minipedia/Creazione voce',
// how much characters should have a word to be considered too much lon
// this somehow help people with dyslexia
longWordLen: 13,
// maximum number of acceptable complex words to help people with dyslexia
maxComplexWords: 10,
// min and max number of suggested words to somehow mitigate attention span problems
maxWords: 700,
// maximum number of suggested newlines
maxTotalLines: 180,
// localization stuff
L10N: {
minipedia: "Minipedia",
minipediaShort: "Mini",
normalpedia: "Wikipedia Test",
normalpediaShort: "WikipediaTest",
openMinipedia: "Apri Minipedia",
openNormalpedia: "Apri Wikipedia Test",
createMinipediaPageTitle: "Accesso Minipedia",
createMinipediaPageBody: "Sii il primo a creare una versione più ridotta e più accessibile di questa voce, in Minipedia!",
statsTitle: "Mini Report",
statsHeadingSubject: "Fattore",
statsHeadingValue: "Valore attuale",
statsHeadingExpected: "Limite consigliato",
statsHeadingSimplicity: "Semplicità",
statsWords: "Parole",
statsLines: "Paragrafi",
statsComplexWords: "Parole complesse",
},
};
// global configuration
window.MiniPedia = window.MiniPedia || {};
// shortcut
var mp = window.MiniPedia;
// inherit default configurations
$.extend( true, mp, DEFAULTS );
// another shortcut
var L10N = mp.L10N;
/**
* Lazy shortcut to obtain just the first API result
*
* @param {Object} response API Response
* @return {Object} page object
*/
function justFirstQueryPage( response ) {
// no response no party
if( !response.query || !response.query.pages ) {
throw 'no valid API response';
}
// the list should contain just one page
var pages = response.query.pages;
for( var id in pages ) {
return pages[ id ];
}
// no page no party
return false;
};
/**
* Get a fresh MediaWiki API object
*
* @return mw.Api
*/
function mwApi() {
return mw.loader.using( 'mediawiki.api' ).then( function() {
return new mw.Api();
} );
};
/**
* Check if a page title already exists
*
* @param title Page title
* @return Promise
*/
function pageExists( title ) {
// prepare the API request
var request = {
action: 'query',
prop: 'info',
titles: title,
};
// eventually load API stuff
return mwApi().then( function( api ) {
// make the API request
return api.get( request ).then( function ( response ) {
// check if it exists
var page = justFirstQueryPage( response );
if( page && page.pageid && page.pageid > 0 ) {
return page;
}
return false;
} );
} );
};
/**
* Query the current page plain text
*
* @param {String} Page name (or none for the current one)
* @return Promise
*/
function queryPlainText( pageName ) {
// complete page title with namespace
pageName = pageName || mw.config.get( 'wgPageName' );
// prepare the API request
// See https://phabricator.wikimedia.org/T259332
var request = {
action: 'query',
prop: 'extracts',
titles: pageName,
explaintext: 1,
exlimit: 1,
exsectionformat: 'plain',
};
// this will return a Promise resolving the page plain text, returned upstream
return mwApi().then( function( api ) {
// make the API request and return a Promise
return api.get( request ).then( function( response ) {
// resolve the Promise with the page content
var page = justFirstQueryPage( response );
if( page && page.pageid && page.pageid > 0 && page.extract ) {
return page.extract;
}
// no content
return false;
} );
} );
};
/**
* Prepare the wiki
*/
function prepareNormalpedia() {
// normal page title and mini version
var pageName = mw.config.get( 'wgPageName' );
// prepare the Minipedia title object
var miniTitleObject = new mw.Title( pageName, mp.namespaceNum );
// minipedia page title with prefix
var miniPageName = miniTitleObject.getPrefixedText();
// minipedia page URL
var miniPageUrl = miniTitleObject.getUrl();
/**
* Go to the Minipedia page in edit mode
*
* The page in the main namespace will be preloaded.
*/
function goToMinipediaEditPage() {
/**
* Build the query string to edit a page
*
* TODO: eventually add VisualEditor support
*/
var editPageQueryString = {
action: 'edit',
title: miniPageName,
preload: pageName,
editintro: mp.editIntro,
};
// '/index.php'
var wgScript = mw.config.get( 'wgScript' );
// go to the edit page URL
window.location = wgScript + '?' + $.param( editPageQueryString );
};
// add a "Minipedia"
var miniVersionPortletLink = mw.util.addPortletLink(
'p-namespaces',
miniPageUrl,
L10N.minipediaShort,
'ca-minipedia',
L10N.openMinipedia,
'n'
);
// on the mini toolback click, check if a mini version exists
$( miniVersionPortletLink ).click( function( e ) {
// wait for multiple information
$.when(
// check if the page really exists
pageExists( miniPageName ),
// allow to open OO UI windows
mw.loader.using( 'oojs-ui-windows' )
// callback fired when we have all the information
).done( function( miniPageExists, loader ) {
// check if the page already exist
if( miniPageExists ) {
// just redirect to the Minipedia version
window.location = miniPageUrl;
} else {
// ask if you want to create the page
// create message dialog window
var messageDialog = new OO.ui.MessageDialog();
var windowManager = new OO.ui.WindowManager();
$( 'body' ).append( windowManager.$element );
windowManager.addWindows( [ messageDialog ] );
// configure and open dialog
var windowInstance = windowManager.openWindow( messageDialog, {
title: L10N.createMinipediaPageTitle,
message: L10N.createMinipediaPageBody,
} );
// check if you accepted the page creation
windowInstance.closed.then( function ( data ) {
// user is confirming the action
if( data.action === 'accept' ) {
// go go go! to mini
goToMinipediaEditPage();
}
} );
}
// end if page exists
} );
// end $.when()
// avoid scrolling to the top
e.preventDefault();
} );
// end $( miniVersionPortletLink ).click
};
// end prepareNormalWiki()
/**
* Calculate some stats over a text
*/
function textStats( text ) {
var stats = {};
text = text.trim();
// count long words
var totalLongWords = 0;
var word, words = text.split( /\s+/ );
for( var i = 0; i < words.length; i++ ) {
word = words[i];
if( word.length > mp.longWordLen ) {
totalLongWords++;
}
}
// total amount of paragraphs
// paragraphs shorter than this comment are discarded
var totalLines = 0;
var paragraphs = text.split( /\n+/ );
var paragraph;
for( var i = 0; i < paragraphs.length; i++ ) {
paragraph = paragraphs[i].trim();
if( paragraph.length > 30 ) {
totalLines++;
}
}
// how much lines?
stats.totalLines = totalLines;
// how much words?
stats.totalWords = words.length;
// how much of these words are so much long?
// See 'longWordLen'
stats.totalLongWords = totalLongWords;
return stats;
};
/**
* Query content stats of the current page
*
* @param {String} Page title (or nothing for the current one)
* @return Promise
*/
function queryContentStats( pageTitle ) {
return queryPlainText( pageTitle ).then( textStats );
};
/**
* Apply a stupid percentage
*
* @param {int} a
* @param {int} b
* @return The 'b%' applied to 'a'
*/
function applyPercentage( a, b ) {
return parseInt( a * b / 100 );
};
/**
* Prepare the Minipedia stats box
*/
function prepareMinipediaStatsBox() {
// normal page title (non-mini version)
var mainPageTitle = mw.config.get( 'wgTitle' );
// body container
var $contentText = $( '#mw-content-text' );
// prepare the DOM tree
var $container = $( '<div>' );
var $table = $( '<table>' );
var $thead = $( '<thead>' );
var $tbody = $( '<tbody>' );
// prepare the stats container
$container.addClass( 'minipedia-stats' );
// put a title
$container.append( $( '<h2>' ).text( L10N.statsTitle ) );
// put the table
$container.append( $table );
// add table headers
$thead.append(
$( '<tr>' ).append( $( '<th>' ).text( L10N.statsHeadingSubject ) )
.append( $( '<th>' ).text( L10N.statsHeadingValue ) )
.append( $( '<th>' ).text( L10N.statsHeadingExpected ) )
.append( $( '<th>' ).text( L10N.statsHeadingSimplicity ) )
);
// prepare the table
$table.addClass( 'wikitable' )
.append( $thead )
.append( $tbody );
/**
* Append a row (with a label and a value) into a table
*
* The data argument accepts an object with:
*
* className: Class name for the row
* label: Text displayed in row heading (left)
* text: Text displayed in row data (right)
* value Value associated to the text of the mini version
* maxValue Maximum suggested value
* upstreamValue The value of the upstream (main) version
* upstreamLimitPerc The percentage (0-100) applied to the 'upstreamValue' to inherit a suitable 'maxValue'
*
* Note: this function uses OOUI widgets. Make sure to have them loaded.
*
* @param {Object} jQuery table
* @param {Object} Data information
*/
function appendTableStatsRow( $table, data ) {
// read arguments
var className = data.className;
var label = data.label;
var value = data.value;
var maxValue = data.maxValue;
var text = data.text || value;
// eventually calculate the max value
if( !maxValue && data.upstreamValue && data.upstreamLimitPerc ) {
maxValue = applyPercentage( data.upstreamValue, data.upstreamLimitPerc );
}
// prepare table stats row
var $tr = $( '<tr>' );
var $tdLabel = $( '<td>' ).text( label );
var $tdValue = $( '<td>' ).text( text );
var $tdMax = $( '<td>' ).text( maxValue );
var $tdProgress = $( '<td>' );
// build the table row
$tr.addClass( 'minipedia-stats-row-' + className );
$tr.append( $tdLabel )
.append( $tdValue )
.append( $tdMax )
.append( $tdProgress );
// if possible, plot a cute progress bar
if( maxValue ) {
// calculate a 0-100 progress since the value and maxValue
var realPercentage = parseInt( value / maxValue * 100 );
/**
* Calculate the inverse percentage
*
* This is the final percentage shown to the user.
*
* In short, it's always 100% but if you go over
* the limit it start decreasing and reaching zero,
* to rappresent a kind of 'understanding degradation'.
*
* To do not underrate the work of the user it should not go
* below a certain minimum amount. Example: 10%. :^)
*
* Actually the function is just linear.
*/
var inversePercentage = 100;
if( realPercentage > 100 ) {
inversePercentage = Math.max( 10, 200 - realPercentage );
}
// generate the progress bar
var progressBar = new OO.ui.ProgressBarWidget( {
progress: inversePercentage,
} );
// show the progress bar
$tdProgress.append( progressBar.$element );
}
// attach some data to be read by scripts
$tr.data( 'ministats', data );
// eventually emphasize if something is wrong
if( value && maxValue && value > maxValue ) {
$tr.addClass( 'minipedia-stats-row-problem' );
}
// show the row in the stats table
$table.append( $tr );
};
// request multiple stuff at the same time
$.when(
// query the page plain text of the current mini page
queryContentStats(),
// query the page plain text of the related non-mini page
queryContentStats( mainPageTitle ),
// require the progress bar widget
mw.loader.using( 'oojs-ui-widgets' )
// callback fired when we have all the information
).done( function( statsMini, statsMain, mwLoader ) {
// show the stats container at the bottom of the page when we have something
$contentText.append( $container );
// check if we have also some information from the main namespace
// in this case we can do a comparison
if( statsMain ) {
// show the number of words, lines and complex words (actually just long words)
// they can be compared to the main version
appendTableStatsRow( $table, {
className: 'long-words',
label: L10N.statsComplexWords,
value: statsMini.totalLongWords,
upstreamValue: statsMain.totalLongWords,
upstreamLimitPerc: 10,
} );
appendTableStatsRow( $table, {
className: 'words',
label: L10N.statsWords,
value: statsMini.totalWords,
upstreamValue: statsMain.totalWords,
upstreamLimitPerc: 30,
} );
appendTableStatsRow( $table, {
className: 'lines',
label: L10N.statsLines,
value: statsMini.totalLines,
maxValue: statsMain.totalLines,
upstreamLimitPerc: 30,
} );
// there is no the related main page
} else {
// show the number of words, lines and complex words (actually just long words)
// they cannot be compared to the main version
appendTableStatsRow( $table, {
className: 'long-words',
label: L10N.statsComplexWords,
value: statsMini.totalLongWords,
maxValue: mp.maxComplexWords,
} );
appendTableStatsRow( $table, {
className: 'words',
label: L10N.statsWords,
value: statsMini.totalWords,
maxValue: mp.maxStatsWords,
} );
appendTableStatsRow( $table, {
className: 'lines',
label: L10N.statsLines,
value: statsMini.totalLines,
maxValue: mp.maxTotalLines,
} );
}
} );
};
/**
* Prepare the Minipedia namespace
*/
function prepareMinipedia() {
// action of the page (edit, view etc.)
var action = mw.config.get( 'wgAction' );
// normal page title and mini version
var pageName = mw.config.get( 'wgTitle' );
// prepare the Normalpedia title object
var normalTitleObject = new mw.Title( pageName );
// minipedia page title with prefix
var normalPageName = normalTitleObject.getPrefixedText();
// minipedia page URL
var normalPageUrl = normalTitleObject.getUrl();
// add a "Minipedia"
var normalVersionPortletLink = mw.util.addPortletLink(
'p-namespaces',
normalPageUrl,
L10N.normalpediaShort,
'ca-normalpedia',
L10N.openNormalpedia
);
// check if we are in view mode
if( action === 'view' ) {
// in view mode we can fetch the stats
prepareMinipediaStatsBox();
}
};
// end prepareMinipedia()
// work only in the main namespace
var ns = mw.config.get( 'wgNamespaceNumber' );
if( ns === 0 ) {
prepareNormalpedia();
} else if( ns == mp.namespaceNum ) {
prepareMinipedia();
}
// end namespace zero check
} )( $, mw );