User:Lucas Werkmeister/m3api-ApiSandbox-helper.js
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)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/**
* m3api-ApiSandbox-helper
* =======================
*
* See [[User:Lucas Werkmeister/m3api-ApiSandbox-helper]]
* for information and usage instructions.
* (Just remove the .js from the URL of this page.)
*/
( function m3api_ApiSandbox_helper_iife() {
// paramater types
/** A plain string. (Includes several API types without special handling, e.g. timestamp.) */
var TYPE_STRING = 'string';
/** A set of strings. */
var TYPE_STRING_SET = 'string|set';
/** A number (integer or limit in the API). May fall back to a string if necessary. */
var TYPE_NUMBER = 'number';
/** A set of numbers. */
var TYPE_NUMBER_SET = 'number|set';
/** A namespace (number with comment indicating namespace). */
var TYPE_NAMESPACE = 'namespace';
/** A set of namespaces. */
var TYPE_NAMESPACE_SET = 'namespace|set';
/** A boolean. */
var TYPE_BOOLEAN = 'boolean';
/**
* @var {Object.<string,Map|Promise>}
* For a given action, either a Map of param name to TYPE_* constant,
* or a Promise that will resolve when information for that action will be available.
*/
var paramTypesPerAction = {};
/**
* Set of all the actions (modules) that must be POSTed.
*/
var postActions = new Set();
/**
* Parse a paraminfo API response.
*
* On the initial call, make paramTypes the empty set,
* currentModules a set containing only a single action/module,
* and prefix the empty string.
* The main result will be in paramTypes,
* the return value indicates whether the action/module requires POST.
*
* @param {Object} response The API response.
* @param {Map} paramTypes Map to which param types should be added.
* @param {Set} currentModules Modules whose param names should be inspected.
* @param {string} prefix Prefix for all param names currently inspected.
* @param {boolean} Whether any of the currentModules must be POSTed.
*/
function parseParaminfo( response, paramTypes, currentModules, prefix ) {
var mustBePosted = false;
response.paraminfo.modules.forEach( function ( module ) {
if ( !currentModules.has( module.name ) ) {
return;
}
if ( module.mustbeposted ) {
mustBePosted = true;
}
var modulePrefix = prefix + ( module.prefix || '' );
module.parameters.forEach( function ( parameter ) {
var paramName = modulePrefix + parameter.name;
var multi = !!parameter.multi;
if ( parameter.type === 'boolean' ) {
paramTypes.set( paramName, TYPE_BOOLEAN );
} else if ( parameter.type === 'integer' || parameter.type === 'limit' ) {
paramTypes.set( paramName, multi ? TYPE_NUMBER_SET : TYPE_NUMBER );
} else if ( parameter.type === 'namespace' ) {
paramTypes.set( paramName, multi ? TYPE_NAMESPACE_SET : TYPE_NAMESPACE );
} else {
paramTypes.set( paramName, multi ? TYPE_STRING_SET : TYPE_STRING );
}
if ( !parameter.submodules ) {
return;
}
var submoduleNames = Object.keys( parameter.submodules );
var currentModules = new Set();
submoduleNames.forEach( currentModules.add, currentModules );
submoduleNames.forEach( function ( submoduleName ) {
parseParaminfo(
response,
paramTypes,
currentModules,
modulePrefix + ( parameter.submoduleparamprefix || '' )
);
} );
} );
} );
return mustBePosted;
}
/**
* @var {Map|Promise|null}
* A Map of namespace ID to local name,
* or a Promise that will resolve when namespace information will be available,
* or null if the request has not been made yet.
*/
var namespaces = null;
/**
* Parse namespaces out of an API response.
* Parameters should include query+siteinfo+namespaces,
* query+allmessages+ammessages=blanknamespace,
* and formatversion=2.
*
* @param {Object} response The API response.
* @return {Map} Map of namespace ID to local name.
*/
function parseNamespaces( response ) {
var namespaces = new Map();
for ( var id in response.query.namespaces ) {
namespaces.set( id, response.query.namespaces[ id ].name );
}
if ( !namespaces.get( '0' ) ) {
var message = response.query.allmessages[ 0 ];
if ( message.normalizedname !== 'blanknamespace' ) {
throw new Error( 'Unexpected message in response: ' + mesage.normalizedname );
}
namespaces.set( '0', message.content );
}
return namespaces;
}
/**
* RegExp for param values that are (sets of) numbers.
* Does not guarantee that the numbers are representable in JavaScript.
*/
var numberSetRegExp = /^(?:(?:\x1f(?:[1-9][0-9]*|0))+|(?:[1-9][0-9]*|0)(?:\|(?:[1-9][0-9]*|0))*)$/;
/**
* Try to guess the type of a param from its name and value,
* in case paraminfo is not available yet.
*
* @param {string} paramName
* @param {*} paramValue
* @return {string} TYPE_* constant
*/
function guessType( paramName, paramValue ) {
if ( paramValue === 1 ) {
return TYPE_BOOLEAN;
}
paramValue = String( paramValue );
var multi = paramValue[ 0 ] === '\x1f' || paramValue.indexOf( '|' ) !== -1;
if ( numberSetRegExp.test( paramValue ) ) {
return multi ? TYPE_NUMBER_SET : TYPE_NUMBER;
}
if ( paramName === 'namespace' ) {
return multi ? TYPE_NAMESPACE_SET : TYPE_NAMESPACE;
}
return multi ? TYPE_STRING_SET : TYPE_STRING;
}
/**
* Format the given string as code (string literal, single-quoted).
*
* @param {string} string
* @return {string}
*/
function formatString( string ) {
return "'" + JSON.stringify( string ).slice( 1, -1 ) + "'";
}
/**
* Conservative RegExp for JavaScript identifiers
* (ASCII only, since param names are probably ASCII).
*/
var identifierRegExp = /^(?:[$_a-zA-Z][$_a-zA-Z0-9]*)$/;
/**
* RegExp for JavaScript reserved words
* (including await and yield, since we want to generate code
* that works in async functions and/or generator functions).
*/
var reservedWordRegExp = /^(?:await|break|case|catch|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|if|import|in|instanceof|new|null|return|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)$/;
/**
* Whether the given param name can be used in code unquoted.
*
* @param {string} paramName
* @return {boolean}
*/
function canUnquoteParamName( paramName ) {
return identifierRegExp.test( paramName ) &&
!reservedWordRegExp.test( paramName );
}
/**
* Format the given param name for code.
*
* @param {string} paramName
* @return {string}
*/
function formatParamName( paramName ) {
if ( canUnquoteParamName( paramName ) ) {
return paramName;
} else {
return formatString( paramName );
}
}
/**
* Split the given param value for a set.
*
* @param {string} paramValue
* @return {string[]}
*/
function splitSetParamValue( paramValue ) {
if ( paramValue[ 0 ] === '\x1f' ) {
return paramValue.slice( 1 ).split( '\x1f' );
} else {
return paramValue.split( '|' );
}
}
/**
* Format the given param value as code for a string.
*
* @param {string} paramValue
* @return {string} Code, with trailing comma.
*/
function formatParamValueString( paramValue ) {
return formatString( paramValue ) + ',';
}
/**
* Format the given param value as code for a set of strings.
*
* @param {string} paramValue
* @return {string} Code, no leading indentation,
* following lines assume one tab indentation,
* with trailing comma.
*/
function formatParamValueStringSet( paramValue ) {
var values = splitSetParamValue( paramValue );
// try single line
var code = 'set( ';
for ( var i = 0; i < values.length; i++ ) {
if ( i > 0 ) {
code += ', ';
}
code += formatString( values[ i ] );
}
code += ' ),';
if ( code.length <= 76 ) { // assume tab width 4
return code;
}
// too long, switch to one string per line
code = 'set(\n';
for ( var j = 0; j < values.length; j++ ) {
code += '\t\t' + formatString( values[ j ] ) + ',\n';
}
code += '\t),';
return code;
}
/**
* Format the given number as code,
* taking into account possible loss of precision.
*
* @param {string} number
* @return {string}
*/
function formatNumber( number ) {
if ( parseInt( number, 10 ).toString( 10 ) === number ) {
return number;
} else {
// doesn’t fit into a JS number,
// or just isn’t a number (e.g. "max" for limit params)
return formatString( number );
}
}
/**
* Format the given param value as code for a number.
*
* @param {string} paramValue
* @return {string} Code, with trailing comma.
*/
function formatParamValueNumber( paramValue ) {
return formatNumber( paramValue ) + ',';
}
/**
* Format the given param value as code for a set of numbers.
*
* @param {string} paramValue
* @return {string} Code, no leading indentation,
* following lines assume one tab indentation,
* with trailing comma.
*/
function formatParamValueNumberSet( paramValue ) {
var values = splitSetParamValue( paramValue );
// try single line
var code = 'set( ';
for ( var i = 0; i < values.length; i++ ) {
if ( i > 0 ) {
code += ', ';
}
code += formatNumber( values[ i ] );
}
code += ' ),';
if ( code.length <= 76 ) { // assume tab width 4
return code;
}
// too long, switch to one number per line
code = 'set(\n';
for ( var j = 0; j < values.length; j++ ) {
code += '\t\t' + formatNumber( values[ j ] ) + ',\n';
}
code += '\t),';
return code;
}
/**
* Format the given param value as code for a namespace.
*
* @param {string} paramValue
* @return {string} Code, with trailing comma and possibly comment.
*/
function formatParamValueNamespace( paramValue ) {
var namespace = namespaces instanceof Map ?
namespaces.get( paramValue ) :
undefined;
var code = formatNumber( paramValue ) + ',';
if ( namespace !== undefined ) {
code += ' // ' + namespace;
}
return code;
}
/**
* Format the given param value as code for a set of namespaces.
*
* @param {string} paramValue
* @return {string} Code, no leading indentation,
* following lines assume one tab indentation,
* with trailing comma.
*/
function formatParamValueNamespaceSet( paramValue ) {
if ( !( namespaces instanceof Map ) ) {
return formatParamValueNumberSet( paramValue );
}
var values = splitSetParamValue( paramValue );
var code = 'set(\n';
for ( var i = 0; i < values.length; i++ ) {
code += '\t\t' + formatParamValueNamespace( values[ i ] ) + '\n';
}
code += '\t),';
return code;
}
/**
* Format the given param value as code for a boolean.
*
* @param {*} paramValue
* @return {string} Code, with trailing comma.
*/
function formatParamValueBoolean( paramValue ) {
return 'true,'; // any value means true, we can’t tell if it’s absent
}
/**
* Format the given param value as code,
* depending on its type as indicated by the action and param name.
*
* @param {string} action The action param value.
* @param {string} paramName The param name.
* @param {*} paramValue The param value.
* @return {string} Code, no leading indentation,
* following lines assume one tab indentation,
* with trailing comma.
*/
function formatParamValue( action, paramName, paramValue ) {
var type;
if ( paramTypesPerAction[ action ] instanceof Map ) {
type = paramTypesPerAction[ action ].get( paramName );
}
if ( type === undefined ) {
type = guessType( paramName, paramValue );
}
switch ( type ) {
case TYPE_STRING: return formatParamValueString( paramValue );
case TYPE_STRING_SET: return formatParamValueStringSet( paramValue );
case TYPE_NUMBER: return formatParamValueNumber( paramValue );
case TYPE_NUMBER_SET: return formatParamValueNumberSet( paramValue );
case TYPE_NAMESPACE: return formatParamValueNamespace( paramValue );
case TYPE_NAMESPACE_SET: return formatParamValueNamespaceSet( paramValue );
case TYPE_BOOLEAN: return formatParamValueBoolean( paramValue );
default: throw new Error( 'Unknown param type: ' + type );
}
}
/**
* Format the given param name and value for the given action
* into one or more full lines of code,
* indented (one tab) and with trailing comma and line break.
*
* @param {string} action The action param value, for determining the param type.
* @param {string} paramName The param name.
* @param {*} paramValue The param value.
* @return {string}
*/
function formatParam( action, paramName, paramValue ) {
return '\t' + formatParamName( paramName ) +
': ' + formatParamValue( action, paramName, paramValue ) + '\n';
}
/**
* Param names that we want to make default params.
*/
var defaultParams = new Set();
// IE11 supports new Set() but not new Set( [ ... ] ), so add elements afterwards:
[
'maxlag',
'responselanginfo',
'origin',
'uselang',
'errorformat',
'errorlang',
'errorsuselocal',
'formatversion',
].forEach( defaultParams.add, defaultParams );
/**
* Whether to make the param with the given name
* a default (constructor) param or a regular (request) param.
*
* @param {string} paramName
* @return {boolean}
*/
function makeDefaultParam( paramName ) {
return defaultParams.has( paramName );
}
/**
* RegExp for standard wgServer values.
*/
var standardServerRegExp = /^(?:https:)?\/\//;
/**
* Standard wgScriptPath value.
*/
var standardScriptPath = '/w';
/**
* Format an API URL for the Session constructor.
* Use just the domain if possible, otherwise the full URL.
*
* @param {string} wgServer The wgServer config.
* @param {string} wgScriptPath The wgScriptPath config.
* @param {Location} location The location global,
* needed if the wgServer is protocol-relative.
* @return {string}
*/
function formatApiUrl( wgServer, wgScriptPath, location ) {
if ( wgScriptPath === standardScriptPath ) {
var serverMatch = standardServerRegExp.exec( wgServer );
if ( serverMatch !== null ) {
var domain = wgServer.slice( serverMatch[ 0 ].length );
return domain;
}
}
var origin = wgServer.slice( 0, 2 ) === '//' ?
location.protocol + wgServer :
wgServer;
return origin + wgScriptPath + '/api.php';
}
/**
* Format a request into a full m3api code snippet.
*
* @param {Object} params
* @return {string}
*/
function formatRequest( params ) {
var action = params.action,
defaultParams = {},
requestParams = {};
for ( var paramName in params ) {
if ( paramName === 'format' ) {
continue;
}
var paramValue = params[ paramName ];
if ( makeDefaultParam( paramName ) ) {
defaultParams[ paramName ] = paramValue;
} else {
requestParams[ paramName ] = paramValue;
}
}
var apiUrl = formatApiUrl(
mw.config.get( 'wgServer' ),
mw.config.get( 'wgScriptPath' ),
location
);
var code = 'const session = new Session( ' + formatString( apiUrl ) + ', {\n';
var suggestedDefaultParams = {
formatversion: 'formatversion: 2',
errorformat: "errorformat: 'plaintext'",
};
for ( var defaultParamName in defaultParams ) {
code += formatParam( action, defaultParamName, defaultParams[ defaultParamName ] );
delete suggestedDefaultParams[ defaultParamName ];
}
for ( var suggestedDefaultParamName in suggestedDefaultParams ) {
code += '\t// (suggestion) ' + suggestedDefaultParams[ suggestedDefaultParamName ] + ',\n';
}
code += '}, {\n' +
'\tuserAgent: \'m3api-ApiSandbox-helper\', // change this :)\n' +
'} );\n';
code += 'const response = await session.request( {\n';
for ( var requestParamName in requestParams ) {
code += formatParam( action, requestParamName, requestParams[ requestParamName ] );
}
if ( postActions.has( action ) ) {
code += '}, {\n' +
"\tmethod: 'POST',\n";
}
code += '} );';
return code;
}
mw.hook( 'apisandbox.formatRequest' ).add( function m3api_ApiSandbox_helper( items, displayParams, rawParams ) {
// generate initial code
var code = formatRequest( displayParams );
var copyTextLayout = new mw.widgets.CopyTextLayout( {
label: $( '<a>' )
.attr( 'href', 'https://www.npmjs.com/package/m3api' )
.attr( 'target', '_blank' )
.addClass( 'external' )
.text( 'm3api' )
.add( document.createTextNode( ' code:' ) ),
copyText: code,
multiline: true,
textInput: {
classes: [ 'mw-apisandbox-textInputCode' ],
rows: Math.min( 15, code.split( '\n' ).length ),
},
} );
var menuOptionWidget = new OO.ui.MenuOptionWidget( {
label: 'm3api code',
data: copyTextLayout,
} );
items.push( menuOptionWidget );
// look up paraminfo and/or namespaces if we don’t have them yet
var action = displayParams.action;
if ( !paramTypesPerAction[ action ] ) {
paramTypesPerAction[ action ] = new mw.Api().get( {
action: 'paraminfo',
modules: [ action, action + '+**' ],
formatversion: 2,
} ).then( function ( response ) {
var paramTypes = new Map();
var currentModules = new Set();
currentModules.add( action );
var mustBePosted = parseParaminfo( response, paramTypes, currentModules, '' );
paramTypesPerAction[ action ] = paramTypes;
if ( mustBePosted ) {
postActions.add( action );
}
} );
}
var repeatPromise = null;
if ( typeof paramTypesPerAction[ action ].then === 'function' ) {
repeatPromise = paramTypesPerAction[ action ];
}
if ( !namespaces ) {
namespaces = new mw.Api().get( {
action: 'query',
meta: [ 'siteinfo', 'allmessages' ],
siprop: [ 'namespaces' ],
ammessages: [ 'blanknamespace' ],
amlang: [ mw.config.get( 'wgContentLanguage' ) ],
amenableparser: true,
formatversion: 2,
} ).then( function ( response ) {
namespaces = parseNamespaces( response );
} );
}
if ( typeof namespaces.then === 'function' ) {
repeatPromise = repeatPromise ?
$.when( repeatPromise, namespaces ) :
namespaces;
}
// regenerate code after we have paraminfo and namespaces
if ( repeatPromise !== null ) {
repeatPromise.then( function () {
var code = formatRequest( displayParams );
copyTextLayout.textInput.setValue( code );
copyTextLayout.textInput.$input.attr(
'rows', Math.min( 15, code.split( '\n' ).length ) );
} );
}
} );
} )();