From ba09ba3fdee6525b2ba619668f3545192eeef762 Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Fri, 8 Feb 2013 09:56:44 -0500 Subject: [PATCH] Add mw.message library Change-Id: I12ca84f848c34f1227ee8acdc8bc04bdfd0b2d97 --- Scribunto.php | 1 + common/Hooks.php | 1 + engines/LuaCommon/LuaCommon.php | 1 + engines/LuaCommon/MessageLibrary.php | 74 ++++++ engines/LuaCommon/lualib/mw.message.lua | 231 ++++++++++++++++++ .../engines/LuaCommon/MessageLibraryTest.php | 11 + .../engines/LuaCommon/MessageLibraryTests.lua | 85 +++++++ 7 files changed, 404 insertions(+) create mode 100644 engines/LuaCommon/MessageLibrary.php create mode 100644 engines/LuaCommon/lualib/mw.message.lua create mode 100644 tests/engines/LuaCommon/MessageLibraryTest.php create mode 100644 tests/engines/LuaCommon/MessageLibraryTests.lua diff --git a/Scribunto.php b/Scribunto.php index 3b012a36..d31bca2f 100644 --- a/Scribunto.php +++ b/Scribunto.php @@ -107,6 +107,7 @@ $wgAutoloadClasses['Scribunto_LuaSiteLibrary'] = $dir.'engines/LuaCommon/SiteLib $wgAutoloadClasses['Scribunto_LuaUriLibrary'] = $dir.'engines/LuaCommon/UriLibrary.php'; $wgAutoloadClasses['Scribunto_LuaUstringLibrary'] = $dir.'engines/LuaCommon/UstringLibrary.php'; $wgAutoloadClasses['Scribunto_LuaLanguageLibrary'] = $dir.'engines/LuaCommon/LanguageLibrary.php'; +$wgAutoloadClasses['Scribunto_LuaMessageLibrary'] = $dir.'engines/LuaCommon/MessageLibrary.php'; /***** Configuration *****/ diff --git a/common/Hooks.php b/common/Hooks.php index d5c35d80..aae6f424 100644 --- a/common/Hooks.php +++ b/common/Hooks.php @@ -301,6 +301,7 @@ WIKI; 'engines/LuaCommon/SiteLibraryTest.php', 'engines/LuaCommon/UriLibraryTest.php', 'engines/LuaCommon/UstringLibraryTest.php', + 'engines/LuaCommon/MessageLibraryTest.php', ); foreach ( $tests as $test ) { $files[] = dirname( __FILE__ ) .'/../tests/' . $test; diff --git a/engines/LuaCommon/LuaCommon.php b/engines/LuaCommon/LuaCommon.php index ad645c12..cee68969 100644 --- a/engines/LuaCommon/LuaCommon.php +++ b/engines/LuaCommon/LuaCommon.php @@ -10,6 +10,7 @@ abstract class Scribunto_LuaEngine extends ScribuntoEngineBase { 'mw.uri' => 'Scribunto_LuaUriLibrary', 'mw.ustring' => 'Scribunto_LuaUstringLibrary', 'mw.language' => 'Scribunto_LuaLanguageLibrary', + 'mw.message' => 'Scribunto_LuaMessageLibrary', ); /** diff --git a/engines/LuaCommon/MessageLibrary.php b/engines/LuaCommon/MessageLibrary.php new file mode 100644 index 00000000..8c8589e1 --- /dev/null +++ b/engines/LuaCommon/MessageLibrary.php @@ -0,0 +1,74 @@ + array( $this, 'messageToString' ), + 'check' => array( $this, 'messageCheck' ), + ); + + // Get the correct default language from the parser + if ( $this->getParser() ) { + $lang = $this->getParser()->getTargetLanguage(); + } else { + global $wgContLang; + $lang = $wgContLang; + } + + $this->getEngine()->registerInterface( 'mw.message.lua', $lib, array( + 'lang' => $lang->getCode(), + ) ); + } + + private function makeMessage( $data, $setParams ) { + if ( isset( $data['rawMessage'] ) ) { + $msg = new RawMessage( $data['rawMessage'] ); + } else { + $msg = Message::newFallbackSequence( $data['keys'] ); + } + $msg->inLanguage( $data['lang'] ) + ->useDatabase( $data['useDB'] ); + if ( $setParams ) { + if ( isset( $data['title'] ) ) { + $title = Title::newFromText( $data['title'] ); + } else { + $title = $this->getTitle(); + } + + $msg->params( array_values( $data['params'] ) ) + ->title( $title ); + } + return $msg; + } + + function messageToString( $format, $data ) { + if ( !in_array( $format, array( 'parse', 'text', 'plain', 'escaped', 'parseAsBlock' ) ) ) { + throw new Scribunto_LuaError( "invalid format for 'messageToString'" ); + } + + if ( in_array( $format, array( 'parse', 'parseAsBlock' ) ) ) { + // Limit calls into the full parser + $this->incrementExpensiveFunctionCount(); + } + + try { + $msg = $this->makeMessage( $data, true ); + return array( call_user_func( array( $msg, $format ) ) ); + } catch( MWException $ex ) { + throw new Scribunto_LuaError( "msg:$format() failed (" . $ex->getMessage() . ")" ); + } + } + + function messageCheck( $what, $data ) { + if ( !in_array( $what, array( 'exists', 'isBlank', 'isDisabled' ) ) ) { + throw new Scribunto_LuaError( "invalid what for 'messageCheck'" ); + } + + try { + $msg = $this->makeMessage( $data, false ); + return array( call_user_func( array( $msg, $what ) ) ); + } catch( MWException $ex ) { + throw new Scribunto_LuaError( "msg:$what() failed (" . $ex->getMessage() . ")" ); + } + } +} diff --git a/engines/LuaCommon/lualib/mw.message.lua b/engines/LuaCommon/lualib/mw.message.lua new file mode 100644 index 00000000..d8fa4ed9 --- /dev/null +++ b/engines/LuaCommon/lualib/mw.message.lua @@ -0,0 +1,231 @@ +local message = {} +local php + +local util = require 'libraryUtil' +local checkType = util.checkType + +local valuemt = { + __tostring = function ( t ) + return tostring( t.raw or t.num ) + end +} + +local function checkScalar( name, argIdx, arg, level, valuemtOk ) + local tp = type( arg ) + + -- If special params are ok, detect them + if valuemtOk and tp == 'table' and getmetatable( arg ) == valuemt then + return arg + end + + -- If it's a table with a custom __tostring function, use that string + if tp == 'table' and getmetatable( arg ) and getmetatable( arg ).__tostring then + return tostring( arg ) + end + + if tp ~= 'string' and tp ~= 'number' then + error( string.format( + "bad argument #%d to '%s' (string or number expected, got %s)", + argIdx, name, tp + ), level + 1 ) + end + + return arg +end + +local function checkParams( name, valueOk, ... ) + -- Accept an array of params, or params as individual command line arguments + local params, nparams + local first = select( 1, ... ) + if type( first ) == 'table' and + not ( getmetatable( first ) and getmetatable( first ).__tostring ) + then + if select( '#', ... ) == 1 then + params = first + nparams = table.maxn( params ) + else + error( + "bad arguments to '" .. name .. "' (pass either a table of params or params as individual arguments)", + 3 + ) + end + else + params = { ... } + nparams = select( '#', ... ) + end + for i = 1, nparams do + params[i] = checkScalar( 'params', i, params[i], 3, valueOk ) + end + return params +end + +function message.setupInterface( options ) + -- Boilerplate + message.setupInterface = nil + php = mw_interface + mw_interface = nil + php.options = options + + -- Register this library in the "mw" global + mw = mw or {} + mw.message = message + + package.loaded['mw.message'] = message +end + +local function makeMessage( options ) + local obj = {} + local checkSelf = util.makeCheckSelfFunction( 'mw.message', 'msg', obj, 'message object' ) + + local data = { + keys = options.keys, + rawMessage = options.rawMessage, + params = {}, + lang = php.options.lang, + useDB = true, + } + local funcs = {} + + function funcs:params( ... ) + checkSelf( self, 'params' ) + local params = checkParams( 'params', true, ... ) + local j = #data.params + for i = 1, #params do + data.params[j + i] = params[i] + end + return self + end + + function funcs:rawParams( ... ) + checkSelf( self, 'rawParams' ) + local params = checkParams( 'rawParams', false, ... ) + local j = #data.params + for i = 1, #params do + data.params[j + i] = setmetatable( { raw = params[i] }, valuemt ) + end + return self + end + + function funcs:numParams( ... ) + checkSelf( self, 'numParams' ) + local params = checkParams( 'numParams', false, ... ) + local j = #data.params + for i = 1, #params do + data.params[j + i] = setmetatable( { num = params[i] }, valuemt ) + end + return self + end + + function funcs:inLanguage( lang ) + checkSelf( self, 'inLanguage' ) + if type( lang ) == 'table' and lang.getCode then + -- probably a mw.language object + lang = lang:getCode() + end + checkType( 'inLanguage', 1, lang, 'string' ) + data.lang = lang + return self + end + + function funcs:useDatabase( value ) + checkSelf( self, 'useDatabase' ) + checkType( 'useDatabase', 1, value, 'boolean' ) + data.useDB = value + return self + end + + function funcs:title( title ) + checkSelf( self, 'title' ) + if type( title ) == 'table' and title.prefixedText then + -- probably a mw.title object + title = title.prefixedText + end + checkType( 'title', 1, title , 'string', true ) + data.title = title + return self + end + + function funcs:parse() + checkSelf( self, 'parse' ) + return php.toString( 'parse', data ) + end + + function funcs:text() + checkSelf( self, 'text' ) + return php.toString( 'text', data ) + end + + function funcs:plain() + checkSelf( self, 'plain' ) + return php.toString( 'plain', data ) + end + + function funcs:escaped() + checkSelf( self, 'escaped' ) + return php.toString( 'escaped', data ) + end + + function funcs:parseAsBlock() + checkSelf( self, 'parseAsBlock' ) + return php.toString( 'parseAsBlock', data ) + end + + function funcs:exists() + checkSelf( self, 'exists' ) + return php.check( 'exists', data ) + end + + function funcs:isBlank() + checkSelf( self, 'isBlank' ) + return php.check( 'isBlank', data ) + end + + function funcs:isDisabled() + checkSelf( self, 'isDisabled' ) + return php.check( 'isDisabled', data ) + end + + return setmetatable( obj, { + __index = funcs, + __tostring = function ( t ) + return t:text() + end + } ) +end + +function message.new( key, ... ) + checkType( 'message.new', 1, key, 'string' ) + return makeMessage{ keys = { key } }:params( ... ) +end + +function message.newFallbackSequence( ... ) + for i = 1, math.max( 1, select( '#', ... ) ) do + checkType( 'message.newFallbackSequence', i, select( i, ... ), 'string' ) + end + return makeMessage{ keys = { ... } } +end + +function message.newRawMessage( msg, ... ) + checkType( 'message.newRawMessage', 1, msg, 'string' ) + return makeMessage{ rawMessage = msg }:params( ... ) +end + +function message.rawParam( value ) + value = checkScalar( 'message.rawParam', 1, value ) + return setmetatable( { raw = value }, valuemt ) +end + +function message.numParam( value ) + value = checkScalar( 'message.numParam', 1, value ) + return setmetatable( { num = value }, valuemt ) +end + +function message.getDefaultLanguage() + if mw.language then + return mw.language.new( php.options.lang ) + else + return php.options.lang + end +end + +return message diff --git a/tests/engines/LuaCommon/MessageLibraryTest.php b/tests/engines/LuaCommon/MessageLibraryTest.php new file mode 100644 index 00000000..604c9772 --- /dev/null +++ b/tests/engines/LuaCommon/MessageLibraryTest.php @@ -0,0 +1,11 @@ + __DIR__ . '/MessageLibraryTests.lua', + ); + } +} diff --git a/tests/engines/LuaCommon/MessageLibraryTests.lua b/tests/engines/LuaCommon/MessageLibraryTests.lua new file mode 100644 index 00000000..5c41fee1 --- /dev/null +++ b/tests/engines/LuaCommon/MessageLibraryTests.lua @@ -0,0 +1,85 @@ +local testframework = require 'Module:TestFramework' + +local message1 = mw.message.new( 'mainpage' ) +local message1_copy = mw.message.new( 'mainpage' ) +local message2 = mw.message.new( 'i-dont-exist-evar' ) + +function test_exists( key ) + return mw.message.new( key ):exists() +end + +function test_language( key ) + -- If mw.language is available, test that too + local lang = 'ru' + if mw.language then + lang = mw.language.new( 'ru' ) + end + + return mw.message.new( 'mainpage' ):useDatabase( false ):inLanguage( 'en' ):text(), + mw.message.new( 'mainpage' ):useDatabase( false ):inLanguage( 'ru' ):text(), + mw.message.new( 'mainpage' ):useDatabase( false ):inLanguage( lang ):text() +end + +function test_params( rawMessage, func, ... ) + local msg = mw.message.newRawMessage( rawMessage ):inLanguage( 'en' ) + return msg[func]( msg, ... ):parse() +end + +function test_title() + -- If mw.title is available, test that too + local title = 'Main Page' + if mw.title then + title = mw.title.new( title ) + end + + return mw.message.newRawMessage( '{{PAGENAME}}' ):title( 'Main Page' ):text(), + mw.message.newRawMessage( '{{PAGENAME}}' ):title( title ):text() +end + +return testframework.getTestProvider( { + { name = 'exists (1)', func = test_exists, + args = { 'mainpage' }, + expect = { true } + }, + { name = 'exists (2)', func = test_exists, + args = { 'i-dont-exist-evar' }, + expect = { false } + }, + + { name = 'inLanguage', func = test_language, + expect = { 'Main Page', 'Заглавная страница', 'Заглавная страница' } + }, + + { name = 'title', func = test_title, + expect = { 'Main Page', 'Main Page' } + }, + + { name = 'plain param', func = test_params, + args = { '($1 $2)', 'params', "'''foo'''", 123456 }, + expect = { "(foo 123456)" } + }, + { name = 'raw param', func = test_params, + args = { '($1 $2)', 'rawParams', "'''foo'''", 123456 }, + expect = { "('''foo''' 123456)" } + }, + { name = 'num param', func = test_params, + args = { '($1 $2)', 'numParams', "'''foo'''", 123456 }, + expect = { "(foo 123,456)" } + }, + { name = 'mixed params', func = test_params, + args = { '($1 $2 $3)', 'params', + "'''foo'''", mw.message.rawParam( "'''foo'''" ), mw.message.numParam( 123456 ) + }, + expect = { "(foo '''foo''' 123,456)" } + }, + + { name = 'message as param', func = test_params, + args = { '($1)', 'params', mw.message.newRawMessage( 'bar' ) }, + expect = { "(bar)" } + }, + + { name = 'different title', func = test_params, + args = { '($1)', 'params', mw.message.newRawMessage( 'bar' ) }, + expect = { "(bar)" } + }, +} )