Difference between revisions of "MediaWiki:Gadget-minipedia.js"
Jump to navigation
Jump to search
(work in progress) |
(refactor createTitle()) |
||
| (22 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/** |
/** |
||
| + | * Make Minipedia magics |
||
| − | * Add "Purge" content action link. |
||
* |
* |
||
* Dependencies: mediawiki.util |
* Dependencies: mediawiki.util |
||
| + | * Author: 2020-2025 [[User:ValerioBoz-WMCH]] |
||
* |
* |
||
| + | * See https://phabricator.wikimedia.org/tag/minipedia/ |
||
| − | * @source www.mediawiki.org/wiki/Snippets/Purge_action |
||
| − | * @revision 2014-05-14 |
||
*/ |
*/ |
||
| + | ( function ( mw, $ ) { |
||
| − | $.when( mw.loader.using( [ 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core' ] ), $.ready ).then( function () { |
||
| + | /* |
||
| − | // global configuration file |
||
| + | * 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, |
||
| + | |||
| + | // namespace prefix configured in your LocalSettings.php |
||
| + | namespaceIntermediate: 'Intermediate', |
||
| + | |||
| + | // namespace number configured in your LocalSettings.php |
||
| + | namespaceIntermediateNum: 3004, |
||
| + | |||
| + | // namespace prefix configured in your LocalSettings.php |
||
| + | namespaceAdvanced: 'Intermediate', |
||
| + | |||
| + | // namespace number configured in your LocalSettings.php |
||
| + | namespaceAdvancedNum: 3006, |
||
| + | |||
| + | // 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", |
||
| + | minipediaShortIntermediate: "Intermediate", |
||
| + | minipediaShortAdvanced: "Advanced", |
||
| + | normalpedia: "Wikipedia Test", |
||
| + | normalpediaShort: "WikipediaTest", |
||
| + | openMinipedia: "Apri Minipedia", |
||
| + | openMinipediaIntermediate: "Open Minipedia Intermediate", |
||
| + | openMinipediaAdvanced: "Open Minipedia Advanced", |
||
| + | 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 || {}; |
window.MiniPedia = window.MiniPedia || {}; |
||
| + | // shortcut |
||
| − | // shortcut for the global configuration file (mp = mini-pedia) |
||
var mp = window.MiniPedia; |
var mp = window.MiniPedia; |
||
| + | // inherit default configurations |
||
| − | // default namespace name |
||
| + | $.extend( true, mp, DEFAULTS ); |
||
| − | mp.namespace = mp.namespace || 'Mini'; |
||
| + | // another shortcut |
||
| − | // work only in the main namespace |
||
| − | var |
+ | var L10N = mp.L10N; |
| − | if( ns !== 0 ) { |
||
| − | return; |
||
| − | } |
||
| + | /** |
||
| − | // add a "Minipedia" |
||
| + | * Lazy shortcut to obtain just the first API result |
||
| − | var miniVersionPortletLink = mw.util.addPortletLink( |
||
| + | * |
||
| − | 'p-cactions', |
||
| + | * @param {Object} response API Response |
||
| − | '#', |
||
| + | * @return {Object} page object |
||
| − | 'Mini Version', |
||
| + | */ |
||
| − | 'ca-minipedia', |
||
| + | function justFirstQueryPage( response ) { |
||
| − | 'Open Minipedia', |
||
| − | 'n' |
||
| − | ); |
||
| + | // no response no party |
||
| − | var pageName = mw.config.get( 'wgPageName' ); |
||
| + | if( !response.query || !response.query.pages ) { |
||
| − | var miniPageName = mp.namespace + ':' + pageName; |
||
| + | 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 |
* Check if a page title already exists |
||
* |
* |
||
| − | * @param title |
+ | * @param title Page title |
| + | * @return Promise |
||
| − | * @param callback Callback with one boolean argument telling if the page title exists or not |
||
*/ |
*/ |
||
| − | function pageExists( title |
+ | function pageExists( title ) { |
// prepare the API request |
// prepare the API request |
||
| Line 49: | Line 142: | ||
action: 'query', |
action: 'query', |
||
prop: 'info', |
prop: 'info', |
||
| − | titles: title |
+ | titles: title, |
}; |
}; |
||
| − | // |
+ | // eventually load API stuff |
| − | + | return mwApi().then( function( api ) { |
|
| − | // |
+ | // make the API request |
| + | return api.get( request ).then( function ( response ) { |
||
| − | if( !response.query ) { |
||
| − | throw 'no response'; |
||
| − | } |
||
| + | // check if it exists |
||
| − | var page; |
||
| − | + | var page = justFirstQueryPage( response ); |
|
| − | page |
+ | if( page && page.pageid && page.pageid > 0 ) { |
| + | return page; |
||
| − | if( page.pageid && page.pageid > 0 ) { |
||
| + | } |
||
| − | + | 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; |
|
| + | |||
| + | } ); |
||
} ); |
} ); |
||
}; |
}; |
||
| + | /** |
||
| − | // on the mini toolback click, check if a mini version exists |
||
| + | * Create a new Mini namespace object. |
||
| − | $( miniVersionPortletLink ).click( function() { |
||
| + | */ |
||
| + | function Mini(ns, shortLabel, openLabel, emoji) { |
||
| + | this.ns = ns; |
||
| + | this.shortLabel = shortLabel; |
||
| + | this.openLabel = openLabel; |
||
| + | this.emoji = emoji; |
||
| + | }; |
||
| + | /** |
||
| − | // check if the mini version exists |
||
| + | * Check if the current namespace is mine. |
||
| − | pageExists( miniPageName, function( miniPageExists ) { |
||
| + | * @return boolean |
||
| + | */ |
||
| + | Mini.prototype.isCurrentNamespace = function() { |
||
| + | return this.ns == mw.config.get( 'wgNamespaceNumber' ); |
||
| + | }; |
||
| + | |||
| + | /** |
||
| + | * @return mw.Title |
||
| + | */ |
||
| + | Mini.prototype.createTitle = function(pageName) { |
||
| + | var pageName = pageName || mw.config.get( 'wgTitle' ); |
||
| + | return new mw.Title(pageName, this.ns); |
||
| + | }; |
||
| + | |||
| + | /** |
||
| + | * Register a namespace portlet button. |
||
| + | */ |
||
| + | Mini.prototype.registerPortlet = function() { |
||
| + | // Don't load on this very same namespace. |
||
| + | if (this.isCurrentNamespace()) { |
||
| + | return; |
||
| + | } |
||
| + | |||
| + | // Normal page title and mini version. |
||
| + | var mwTitle = this.createTitle(); |
||
| + | var portlet = mw.util.addPortletLink( |
||
| + | 'p-namespaces', |
||
| + | mwTitle.getUrl(), |
||
| + | this.emoji + ' ' + this.shortLabel, |
||
| + | 'ca-minipedia', |
||
| + | this.openLabel, |
||
| + | 'n' |
||
| + | ); |
||
| + | |||
| + | var that = this; |
||
| + | $(portlet).click( function(e) { |
||
| + | that.onPortletClick(e); |
||
| + | } ); |
||
| + | |||
| + | return portlet; |
||
| + | }; |
||
| + | |||
| + | Mini.prototype.onPortletClick = function(e) { |
||
| + | var pageTitle = this.createTitle(); |
||
| + | var miniPageName = pageTitle.getPrefixedText(); |
||
| + | var miniPageUrl = pageTitle.getUrl(); |
||
| + | var that = this; |
||
| + | // 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 ) { |
if( miniPageExists ) { |
||
| + | |||
| − | // do something |
||
| + | // just redirect to the Minipedia version |
||
| + | window.location = miniPageUrl; |
||
} else { |
} else { |
||
| + | |||
| − | // do something 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 |
||
| + | that.onMinipediaConfirmation(); |
||
| + | } |
||
| + | } ); |
||
| + | |||
} |
} |
||
| + | // end if page exists |
||
} ); |
} ); |
||
| + | // end $.when() |
||
| − | } ); |
||
| + | |||
| − | } ); |
||
| + | // avoid scrolling to the top |
||
| + | e.preventDefault(); |
||
| + | }; |
||
| + | |||
| + | Mini.prototype.onMinipediaConfirmation = function() { |
||
| + | /** |
||
| + | * Go to the Minipedia page in edit mode |
||
| + | * |
||
| + | * The page in the main namespace will be preloaded. |
||
| + | * |
||
| + | */ |
||
| + | var mwTitle = this.createTitle(); |
||
| + | var miniPageName = mwTitle.getPrefixedText(); |
||
| + | var pageName = mw.config.get( 'wgPageName' ); |
||
| + | |||
| + | /** |
||
| + | * 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 ); |
||
| + | }; |
||
| + | |||
| + | /** |
||
| + | * Prepare the wiki |
||
| + | */ |
||
| + | function prepareNormalpedia() { |
||
| + | var miniElementary = new Mini( |
||
| + | mp.namespaceNum, |
||
| + | L10N.minipediaShort, |
||
| + | L10N.openMinipedia, |
||
| + | '🐣'); |
||
| + | var miniIntermediate = new Mini( |
||
| + | mp.namespaceIntermediateNum, |
||
| + | L10N.minipediaShortIntermediate, |
||
| + | L10N.openMinipediaIntermediate, |
||
| + | '📚'); |
||
| + | var miniAdvanced = new Mini( |
||
| + | mp.namespaceAdvancedNum, |
||
| + | L10N.minipediaShortAdvanced, |
||
| + | L10N.openMinipediaAdvanced, |
||
| + | '🔬'); |
||
| + | |||
| + | // add a "Minipedia" |
||
| + | var miniVersionPortletLink = miniElementary.registerPortlet(); |
||
| + | |||
| + | // add a "Minipedia Intermediate" |
||
| + | var miniVersionPortletLinkIntermediate = miniIntermediate.registerPortlet(); |
||
| + | |||
| + | // add a "Minipedia Advanced" |
||
| + | var miniVersionPortletLinkAdvanced = miniAdvanced.registerPortlet(); |
||
| + | |||
| + | }; |
||
| + | // 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 || ns == mp.namespaceIntermediateNum || ns == mp.namespaceAdvancedNum ) { |
||
| + | prepareNormalpedia(); |
||
| + | prepareMinipedia(); |
||
| + | } |
||
| + | // end namespace zero check |
||
| + | |||
| + | } )( mw, $ ); |
||
Latest revision as of 17:38, 16 May 2025
/**
* Make Minipedia magics
*
* Dependencies: mediawiki.util
* Author: 2020-2025 [[User:ValerioBoz-WMCH]]
*
* See https://phabricator.wikimedia.org/tag/minipedia/
*/
( 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,
// namespace prefix configured in your LocalSettings.php
namespaceIntermediate: 'Intermediate',
// namespace number configured in your LocalSettings.php
namespaceIntermediateNum: 3004,
// namespace prefix configured in your LocalSettings.php
namespaceAdvanced: 'Intermediate',
// namespace number configured in your LocalSettings.php
namespaceAdvancedNum: 3006,
// 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",
minipediaShortIntermediate: "Intermediate",
minipediaShortAdvanced: "Advanced",
normalpedia: "Wikipedia Test",
normalpediaShort: "WikipediaTest",
openMinipedia: "Apri Minipedia",
openMinipediaIntermediate: "Open Minipedia Intermediate",
openMinipediaAdvanced: "Open Minipedia Advanced",
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;
} );
} );
};
/**
* Create a new Mini namespace object.
*/
function Mini(ns, shortLabel, openLabel, emoji) {
this.ns = ns;
this.shortLabel = shortLabel;
this.openLabel = openLabel;
this.emoji = emoji;
};
/**
* Check if the current namespace is mine.
* @return boolean
*/
Mini.prototype.isCurrentNamespace = function() {
return this.ns == mw.config.get( 'wgNamespaceNumber' );
};
/**
* @return mw.Title
*/
Mini.prototype.createTitle = function(pageName) {
var pageName = pageName || mw.config.get( 'wgTitle' );
return new mw.Title(pageName, this.ns);
};
/**
* Register a namespace portlet button.
*/
Mini.prototype.registerPortlet = function() {
// Don't load on this very same namespace.
if (this.isCurrentNamespace()) {
return;
}
// Normal page title and mini version.
var mwTitle = this.createTitle();
var portlet = mw.util.addPortletLink(
'p-namespaces',
mwTitle.getUrl(),
this.emoji + ' ' + this.shortLabel,
'ca-minipedia',
this.openLabel,
'n'
);
var that = this;
$(portlet).click( function(e) {
that.onPortletClick(e);
} );
return portlet;
};
Mini.prototype.onPortletClick = function(e) {
var pageTitle = this.createTitle();
var miniPageName = pageTitle.getPrefixedText();
var miniPageUrl = pageTitle.getUrl();
var that = this;
// 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
that.onMinipediaConfirmation();
}
} );
}
// end if page exists
} );
// end $.when()
// avoid scrolling to the top
e.preventDefault();
};
Mini.prototype.onMinipediaConfirmation = function() {
/**
* Go to the Minipedia page in edit mode
*
* The page in the main namespace will be preloaded.
*
*/
var mwTitle = this.createTitle();
var miniPageName = mwTitle.getPrefixedText();
var pageName = mw.config.get( 'wgPageName' );
/**
* 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 );
};
/**
* Prepare the wiki
*/
function prepareNormalpedia() {
var miniElementary = new Mini(
mp.namespaceNum,
L10N.minipediaShort,
L10N.openMinipedia,
'🐣');
var miniIntermediate = new Mini(
mp.namespaceIntermediateNum,
L10N.minipediaShortIntermediate,
L10N.openMinipediaIntermediate,
'📚');
var miniAdvanced = new Mini(
mp.namespaceAdvancedNum,
L10N.minipediaShortAdvanced,
L10N.openMinipediaAdvanced,
'🔬');
// add a "Minipedia"
var miniVersionPortletLink = miniElementary.registerPortlet();
// add a "Minipedia Intermediate"
var miniVersionPortletLinkIntermediate = miniIntermediate.registerPortlet();
// add a "Minipedia Advanced"
var miniVersionPortletLinkAdvanced = miniAdvanced.registerPortlet();
};
// 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 || ns == mp.namespaceIntermediateNum || ns == mp.namespaceAdvancedNum ) {
prepareNormalpedia();
prepareMinipedia();
}
// end namespace zero check
} )( mw, $ );