From 3041b5c038216ce4520c5fffb87fec5f8ed2db9d Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Wed, 2 Nov 2016 12:53:19 -0400 Subject: [PATCH] Add Message::listParam() This allows for passing a list of values that will be turned into a list in the context of the language for which the Message is being processed. For example, currently you'd have to do $msg = new Message( 'something', [ $language->commaList( $list ) ] ); which isn't going to give correct results if the message is later changed to a different language with a different value for 'comma-separator'. Now, you can do this instead $msg = new Message( 'something', [ Message::listParam( $list, 'comma' ) ] ); and it will be listified properly no matter what language is later used to parse $msg. Change-Id: I66868c61832260870449998fef14c842f17753ee --- includes/Message.php | 77 +++++++++++++++++++ includes/api/ApiPageSet.php | 8 +- tests/phpunit/includes/MessageTest.php | 102 +++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 7 deletions(-) diff --git a/includes/Message.php b/includes/Message.php index 85c78d640c..7e5cc7dca2 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -168,6 +168,17 @@ class Message implements MessageSpecifier, Serializable { /** Transform {{..}} constructs, HTML-escape the result */ const FORMAT_ESCAPED = 'escaped'; + /** + * Mapping from Message::listParam() types to Language methods. + * @var array + */ + protected static $listTypeMap = [ + 'comma' => 'commaList', + 'semicolon' => 'semicolonList', + 'pipe' => 'pipeList', + 'text' => 'listToText', + ]; + /** * In which language to get this message. True, which is the default, * means the current user language, false content language. @@ -1070,6 +1081,22 @@ class Message implements MessageSpecifier, Serializable { return [ 'plaintext' => $plaintext ]; } + /** + * @since 1.29 + * + * @param array $list + * @param string $type 'comma', 'semicolon', 'pipe', 'text' + * @return array Array with "list" and "type" keys. + */ + public static function listParam( array $list, $type = 'text' ) { + if ( !isset( self::$listTypeMap[$type] ) ) { + throw new InvalidArgumentException( + "Invalid type '$type'. Known types are: " . join( ', ', array_keys( self::$listTypeMap ) ) + ); + } + return [ 'list' => $list, 'type' => $type ]; + } + /** * Substitutes any parameters into the message text. * @@ -1123,6 +1150,8 @@ class Message implements MessageSpecifier, Serializable { return [ 'before', $this->getLanguage()->formatBitrate( $param['bitrate'] ) ]; } elseif ( isset( $param['plaintext'] ) ) { return [ 'after', $this->formatPlaintext( $param['plaintext'], $format ) ]; + } elseif ( isset( $param['list'] ) ) { + return $this->formatListParam( $param['list'], $param['type'], $format ); } else { $warning = 'Invalid parameter for message "' . $this->getKey() . '": ' . htmlspecialchars( serialize( $param ) ); @@ -1251,6 +1280,54 @@ class Message implements MessageSpecifier, Serializable { } } + + /** + * Formats a list of parameters as a concatenated string. + * @since 1.29 + * @param array $params + * @param string $listType + * @param string $format One of the FORMAT_* constants. + * @return array Array with the parameter type (either "before" or "after") and the value. + */ + protected function formatListParam( array $params, $listType, $format ) { + if ( !isset( self::$listTypeMap[$listType] ) ) { + $warning = 'Invalid list type for message "' . $this->getKey() . '": ' . + htmlspecialchars( serialize( $param ) ); + trigger_error( $warning, E_USER_WARNING ); + $e = new Exception; + wfDebugLog( 'Bug58676', $warning . "\n" . $e->getTraceAsString() ); + return [ 'before', '[INVALID]' ]; + } + $func = self::$listTypeMap[$listType]; + + // Handle an empty list sensibly + if ( !$params ) { + return [ 'before', $this->getLanguage()->$func( [] ) ]; + } + + // First, determine what kinds of list items we have + $types = []; + $vars = []; + $list = []; + foreach ( $params as $n => $p ) { + list( $type, $value ) = $this->extractParam( $p, $format ); + $types[$type] = true; + $list[] = $value; + $vars[] = '$' . ( $n + 1 ); + } + + // Easy case: all are 'before' or 'after', so just join the + // values and use the same type. + if ( count( $types ) === 1 ) { + return [ key( $types ), $this->getLanguage()->$func( $list ) ]; + } + + // Hard case: We need to process each value per its type, then + // return the concatenated values as 'after'. We handle this by turning + // the list into a RawMessage and processing that as a parameter. + $vars = $this->getLanguage()->$func( $vars ); + return $this->extractParam( new RawMessage( $vars, $params ), $format ); + } } /** diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 1a509c5c1e..853a8056be 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -1412,13 +1412,7 @@ class ApiPageSet extends ApiBase { ApiBase::PARAM_DFLT => false, ApiBase::PARAM_HELP_MSG => [ 'api-pageset-param-converttitles', - new DeferredStringifier( - function ( IContextSource $context ) { - return $context->getLanguage() - ->commaList( LanguageConverter::$languagesWithVariants ); - }, - $this - ) + [ Message::listParam( LanguageConverter::$languagesWithVariants, 'text' ) ], ], ], ]; diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php index e8afb4ccee..4fe806c438 100644 --- a/tests/phpunit/includes/MessageTest.php +++ b/tests/phpunit/includes/MessageTest.php @@ -512,6 +512,108 @@ class MessageTest extends MediaWikiLangTestCase { ); } + public static function provideListParam() { + $lang = Language::factory( 'de' ); + $msg1 = new Message( 'mainpage', [], $lang ); + $msg2 = new RawMessage( "''link''", [], $lang ); + + return [ + 'Simple comma list' => [ + [ 'a', 'b', 'c' ], + 'comma', + 'text', + 'a, b, c' + ], + + 'Simple semicolon list' => [ + [ 'a', 'b', 'c' ], + 'semicolon', + 'text', + 'a; b; c' + ], + + 'Simple pipe list' => [ + [ 'a', 'b', 'c' ], + 'pipe', + 'text', + 'a | b | c' + ], + + 'Simple text list' => [ + [ 'a', 'b', 'c' ], + 'text', + 'text', + 'a, b and c' + ], + + 'Empty list' => [ + [], + 'comma', + 'text', + '' + ], + + 'List with all "before" params, ->text()' => [ + [ "''link''", Message::numParam( 12345678 ) ], + 'semicolon', + 'text', + '\'\'link\'\'; 12,345,678' + ], + + 'List with all "before" params, ->parse()' => [ + [ "''link''", Message::numParam( 12345678 ) ], + 'semicolon', + 'parse', + 'link; 12,345,678' + ], + + 'List with all "after" params, ->text()' => [ + [ $msg1, $msg2, Message::rawParam( '[[foo]]' ) ], + 'semicolon', + 'text', + 'Main Page; \'\'link\'\'; [[foo]]' + ], + + 'List with all "after" params, ->parse()' => [ + [ $msg1, $msg2, Message::rawParam( '[[foo]]' ) ], + 'semicolon', + 'parse', + 'Main Page; link; [[foo]]' + ], + + 'List with both "before" and "after" params, ->text()' => [ + [ $msg1, $msg2, Message::rawParam( '[[foo]]' ), "''link''", Message::numParam( 12345678 ) ], + 'semicolon', + 'text', + 'Main Page; \'\'link\'\'; [[foo]]; \'\'link\'\'; 12,345,678' + ], + + 'List with both "before" and "after" params, ->parse()' => [ + [ $msg1, $msg2, Message::rawParam( '[[foo]]' ), "''link''", Message::numParam( 12345678 ) ], + 'semicolon', + 'parse', + 'Main Page; link; [[foo]]; link; 12,345,678' + ], + ]; + } + + /** + * @covers Message::listParam + * @covers Message::extractParam + * @covers Message::formatListParam + * @dataProvider provideListParam + */ + public function testListParam( $list, $type, $format, $expect ) { + $lang = Language::factory( 'en' ); + + $msg = new RawMessage( '$1' ); + $msg->params( [ Message::listParam( $list, $type ) ] ); + $this->assertEquals( + $expect, + $msg->inLanguage( $lang )->$format() + ); + } + /** * @covers Message::extractParam */ -- 2.20.1