From: Matthew Flaschen Date: Tue, 12 Feb 2013 04:16:08 +0000 (-0500) Subject: (bug 44459) Implement mw.message.text(): X-Git-Tag: 1.31.0-rc.0~20717 X-Git-Url: https://git.cyclocoop.org/%7B%24admin_url%7Dmembres/%7B%7B%20url_for%28%27vote%27%2C%20idvote=vote.voteid%29%20%7D%7D?a=commitdiff_plain;h=2564de082e;p=lhc%2Fweb%2Fwiklou.git (bug 44459) Implement mw.message.text(): * Change default message format in mediawiki.js from plain to text (this includes behavior of mw.msg shorthand). * Code changes are primarily in jqueryMsg, with minor supporting changes in mediawiki. * Organize tests by public entrypoint (mediawiki or mediawiki.jqueryMsg). * Additional tests and assertions for all public entry points, for new and existing behavior. * Convenience test assertion methods * Add explanatory comments for new and existing code. * Add setting to jqueryMsg for format, defaulting to parse, to preserve existing behavior for direct callers to jqueryMsg's public API. * Since there are now two ways of constructing the abstract syntax tree, add a new parameter to the cache's key scheme. * Minor formatting Change-Id: I0d220692262356a12e2f1c0ce30cf6f090428332 --- diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js index 67a63ca657..183b525e8d 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.js +++ b/resources/mediawiki/mediawiki.jqueryMsg.js @@ -12,7 +12,19 @@ 'SITENAME' : mw.config.get( 'wgSiteName' ) }, messages : mw.messages, - language : mw.language + language : mw.language, + + // Same meaning as in mediawiki.js. + // + // Only 'text', 'parse', and 'escaped' are supported, and the + // actual escaping for 'escaped' is done by other code (generally + // through jqueryMsg). + // + // However, note that this default only + // applies to direct calls to jqueryMsg. The default for mediawiki.js itself + // is 'text', including when it uses jqueryMsg. + format: 'parse' + }; /** @@ -57,7 +69,15 @@ * @return {Function} function suitable for assigning to window.gM */ mw.jqueryMsg.getMessageFunction = function ( options ) { - var failableParserFn = getFailableParserFn( options ); + var failableParserFn = getFailableParserFn( options ), + format; + + if ( options && options.format !== undefined ) { + format = options.format; + } else { + format = parserDefaults.format; + } + /** * N.B. replacements are variadic arguments or an array in second parameter. In other words: * somefunction(a, b, c, d) @@ -69,7 +89,12 @@ * @return {string} Rendered HTML. */ return function () { - return failableParserFn( arguments ).html(); + var failableResult = failableParserFn( arguments ); + if ( format === 'text' || format === 'escaped' ) { + return failableResult.text(); + } else { + return failableResult.html(); + } }; }; @@ -116,12 +141,28 @@ */ mw.jqueryMsg.parser = function ( options ) { this.settings = $.extend( {}, parserDefaults, options ); + this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' ); + this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic ); }; mw.jqueryMsg.parser.prototype = { - // cache, map of mediaWiki message key to the AST of the message. In most cases, the message is a string so this is identical. - // (This is why we would like to move this functionality server-side). + /** + * Cache mapping MediaWiki message keys and the value onlyCurlyBraceTransform, to the AST of the message. + * + * In most cases, the message is a string so this is identical. + * (This is why we would like to move this functionality server-side). + * + * The two parts of the key are separated by colon. For example: + * + * "message-key:true": ast + * + * if they key is "message-key" and onlyCurlyBraceTransform is true. + * + * This cache is shared by all instances of mw.jqueryMsg.parser. + * + * @static + */ astCache: {}, /** @@ -142,16 +183,19 @@ * @return {String|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing */ getAst: function ( key ) { - if ( this.astCache[ key ] === undefined ) { - var wikiText = this.settings.messages.get( key ); + var cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' ), wikiText; + + if ( this.astCache[ cacheKey ] === undefined ) { + wikiText = this.settings.messages.get( key ); if ( typeof wikiText !== 'string' ) { wikiText = '\\[' + key + '\\]'; } - this.astCache[ key ] = this.wikiTextToAst( wikiText ); + this.astCache[ cacheKey ] = this.wikiTextToAst( wikiText ); } - return this.astCache[ key ]; + return this.astCache[ cacheKey ]; }, - /* + + /** * Parses the input wikiText into an abstract syntax tree, essentially an s-expression. * * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already. @@ -163,12 +207,12 @@ */ wikiTextToAst: function ( input ) { var pos, - regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, backslash, anyCharacter, - escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral, + regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets, + backslash, anyCharacter, escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral, whitespace, dollar, digits, openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openLink, closeLink, templateName, pipe, colon, templateContents, openTemplate, closeTemplate, - nonWhitespaceExpression, paramExpression, expression, result; + nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result; // Indicates current position in input as we parse through it. // Shared among all parsing functions below. @@ -274,6 +318,7 @@ regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ ); regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/); regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/); + regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ ); backslash = makeStringParser( '\\' ); anyCharacter = makeRegexParser( /^./ ); function escapedLiteral() { @@ -297,14 +342,14 @@ ] ); // Used to define "literals" without spaces, in space-delimited situations function literalWithoutSpace() { - var result = nOrMore( 1, escapedOrLiteralWithoutSpace )(); - return result === null ? null : result.join(''); + var result = nOrMore( 1, escapedOrLiteralWithoutSpace )(); + return result === null ? null : result.join(''); } // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default // it is not a literal in the parameter function literalWithoutBar() { - var result = nOrMore( 1, escapedOrLiteralWithoutBar )(); - return result === null ? null : result.join(''); + var result = nOrMore( 1, escapedOrLiteralWithoutBar )(); + return result === null ? null : result.join(''); } // Used for wikilink page names. Like literalWithoutBar, but @@ -315,9 +360,15 @@ } function literal() { - var result = nOrMore( 1, escapedOrRegularLiteral )(); - return result === null ? null : result.join(''); + var result = nOrMore( 1, escapedOrRegularLiteral )(); + return result === null ? null : result.join(''); } + + function curlyBraceTransformExpressionLiteral() { + var result = nOrMore( 1, regularLiteralWithSquareBrackets )(); + return result === null ? null : result.join(''); + } + whitespace = makeRegexParser( /^\s+/ ); dollar = makeStringParser( '$' ); digits = makeRegexParser( /^\d+/ ); @@ -498,8 +549,22 @@ literal ] ); - function start() { - var result = nOrMore( 0, expression )(); + // Used when only {{-transformation is wanted, for 'text' + // or 'escaped' formats + curlyBraceTransformExpression = choice( [ + template, + replacement, + curlyBraceTransformExpressionLiteral + ] ); + + + /** + * Starts the parse + * + * @param {Function} rootExpression root parse function + */ + function start( rootExpression ) { + var result = nOrMore( 0, rootExpression )(); if ( result === null ) { return null; } @@ -508,7 +573,9 @@ // everything above this point is supposed to be stateless/static, but // I am deferring the work of turning it into prototypes & objects. It's quite fast enough // finally let's do some actual work... - result = start(); + + // If you add another possible rootExpression, you must update the astCache key scheme. + result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression ); /* * For success, the p must have gotten to the end of the input @@ -792,6 +859,8 @@ // Replace the default message parser with jqueryMsg oldParser = mw.Message.prototype.parser; mw.Message.prototype.parser = function () { + var messageFunction; + // TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe? // Caching is somewhat problematic, because we do need different message functions for different maps, so // we'd have to cache the parser as a member of this.map, which sounds a bit ugly. @@ -800,7 +869,12 @@ // Fall back to mw.msg's simple parser return oldParser.apply( this ); } - var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } ); + + messageFunction = mw.jqueryMsg.getMessageFunction( { + 'messages': this.map, + // For format 'escaped', escaping part is handled by mediawiki.js + 'format': this.format + } ); return messageFunction( this.key, this.parameters ); }; diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index 658487e701..68a3a09995 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -127,7 +127,7 @@ var mw = ( function ( $, undefined ) { * @return Message */ function Message( map, key, parameters ) { - this.format = 'plain'; + this.format = 'text'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : slice.call( parameters ); @@ -136,9 +136,13 @@ var mw = ( function ( $, undefined ) { Message.prototype = { /** - * Simple message parser, does $N replacement and nothing else. + * Simple message parser, does $N replacement, HTML-escaping (only for + * 'escaped' format), and nothing else. + * * This may be overridden to provide a more complex message parser. * + * The primary override is in mediawiki.jqueryMsg. + * * This function will not be called for nonexistent messages. */ parser: function () { @@ -173,14 +177,14 @@ var mw = ( function ( $, undefined ) { if ( !this.exists() ) { // Use as text if key does not exist - if ( this.format !== 'plain' ) { - // format 'escape' and 'parse' need to have the brackets and key html escaped + if ( this.format === 'escaped' || this.format === 'parse' ) { + // format 'escaped' and 'parse' need to have the brackets and key html escaped return mw.html.escape( '<' + this.key + '>' ); } return '<' + this.key + '>'; } - if ( this.format === 'plain' || this.format === 'parse' ) { + if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { text = this.parser(); } @@ -193,7 +197,12 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes format to parse and converts message to string + * Changes format to 'parse' and converts message to string + * + * If jqueryMsg is loaded, this parses the message text from wikitext + * (where supported) to HTML + * + * Otherwise, it is equivalent to plain. * * @return {string} String form of parsed message */ @@ -203,7 +212,10 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes format to plain and converts message to string + * Changes format to 'plain' and converts message to string + * + * This substitutes parameters, but otherwise does not change the + * message text. * * @return {string} String form of plain message */ @@ -213,7 +225,23 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes the format to html escaped and converts message to string + * Changes format to 'text' and converts message to string + * + * If jqueryMsg is loaded, {{-transformation is done where supported + * (such as {{plural:}}, {{gender:}}, {{int:}}). + * + * Otherwise, it is equivalent to plain. + */ + text: function () { + this.format = 'text'; + return this.toString(); + }, + + /** + * Changes the format to 'escaped' and converts message to string + * + * This is equivalent to using the 'text' format (see text method), then + * HTML-escaping the output. * * @return {string} String form of html escaped message */ diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js index 6e9379ee38..6c2a2d4132 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js @@ -1,6 +1,7 @@ ( function ( mw, $ ) { -var mwLanguageCache = {}, oldGetOuterHtml, formatnumTests, specialCharactersPageName; +var mwLanguageCache = {}, oldGetOuterHtml, formatnumTests, specialCharactersPageName, + expectedListUsers, expectedEntrypoints; QUnit.module( 'mediawiki.jqueryMsg', QUnit.newMwEnvironment( { setup: function () { @@ -17,15 +18,36 @@ QUnit.module( 'mediawiki.jqueryMsg', QUnit.newMwEnvironment( { }; // Messages that are reused in multiple tests - // They are also all part of regression tests based on actual extensions. The actual messages have the same key, - // but without jquerymsg-test-. mw.messages.set( { - 'jquerymsg-test-pagetriage-del-talk-page-notify-summary': 'Notifying author of deletion nomination for [[$1]]', - 'jquerymsg-test-categorytree-collapse-bullet': '[−]', - 'jquerymsg-test-wikieditor-toolbar-help-content-signature-result': 'Username (talk)' + // The values for gender are not significant, + // what matters is which of the values is choosen by the parser + 'gender-msg': '$1: {{GENDER:$2|blue|pink|green}}', + + 'plural-msg': 'Found $1 {{PLURAL:$1|item|items}}', + + // Assume the grammar form grammar_case_foo is not valid in any language + 'grammar-msg': 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}', + + 'formatnum-msg': '{{formatnum:$1}}', + + 'portal-url': 'Project:Community portal', + 'see-portal-url': '{{Int:portal-url}} is an important community page.', + + 'jquerymsg-test-statistics-users': '注册[[Special:ListUsers|用户]]', + + 'jquerymsg-test-version-entrypoints-index-php': '[https://www.mediawiki.org/wiki/Manual:index.php index.php]', + + 'external-link-replace': 'Foo [$1 bar]' } ); specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?'; + + expectedListUsers = '注册' + $( '' ).attr( { + title: 'Special:ListUsers', + href: mw.util.wikiGetlink( 'Special:ListUsers' ) + } ).text( '用户' ).getOuterHtml(); + + expectedEntrypoints = 'index.php'; }, teardown: function () { mw.language = this.orgMwLangauge; @@ -105,7 +127,6 @@ QUnit.test( 'Replace', 9, function ( assert ) { 'HTMLElement[] arrays are preserved as raw html' ); - mw.messages.set( 'external-link-replace', 'Foo [$1 bar]' ); assert.equal( parser( 'external-link-replace', 'http://example.org/?x=y&z' ), 'Foo bar', @@ -116,7 +137,6 @@ QUnit.test( 'Replace', 9, function ( assert ) { QUnit.test( 'Plural', 3, function ( assert ) { var parser = mw.jqueryMsg.getMessageFunction(); - mw.messages.set( 'plural-msg', 'Found $1 {{PLURAL:$1|item|items}}' ); assert.equal( parser( 'plural-msg', 0 ), 'Found 0 items', 'Plural test for english with zero as count' ); assert.equal( parser( 'plural-msg', 1 ), 'Found 1 item', 'Singular test for english' ); assert.equal( parser( 'plural-msg', 2 ), 'Found 2 items', 'Plural test for english' ); @@ -128,10 +148,6 @@ QUnit.test( 'Gender', 11, function ( assert ) { var user = mw.user, parser = mw.jqueryMsg.getMessageFunction(); - // The values here are not significant, - // what matters is which of the values is choosen by the parser - mw.messages.set( 'gender-msg', '$1: {{GENDER:$2|blue|pink|green}}' ); - user.options.set( 'gender', 'male' ); assert.equal( parser( 'gender-msg', 'Bob', 'male' ), @@ -199,8 +215,6 @@ QUnit.test( 'Gender', 11, function ( assert ) { QUnit.test( 'Grammar', 2, function ( assert ) { var parser = mw.jqueryMsg.getMessageFunction(); - // Assume the grammar form grammar_case_foo is not valid in any language - mw.messages.set( 'grammar-msg', 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}' ); assert.equal( parser( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar Test with sitename' ); mw.messages.set( 'grammar-msg-wrong-syntax', 'Przeszukaj {{GRAMMAR:grammar_case_xyz}}' ); @@ -230,7 +244,6 @@ QUnit.test( 'Match PHP parser', mw.libs.phpParserData.tests.length, function ( a QUnit.test( 'Links', 6, function ( assert ) { var parser = mw.jqueryMsg.getMessageFunction(), - expectedListUsers, expectedDisambiguationsText, expectedMultipleBars, expectedSpecialCharacters; @@ -240,13 +253,6 @@ QUnit.test( 'Links', 6, function ( assert ) { the bold was removed because it is not yet implemented. */ - mw.messages.set( 'jquerymsg-test-statistics-users', '注册[[Special:ListUsers|用户]]' ); - - expectedListUsers = '注册' + $( '' ).attr( { - title: 'Special:ListUsers', - href: mw.util.wikiGetlink( 'Special:ListUsers' ) - } ).text( '用户' ).getOuterHtml(); - assert.equal( parser( 'jquerymsg-test-statistics-users' ), expectedListUsers, @@ -265,10 +271,9 @@ QUnit.test( 'Links', 6, function ( assert ) { 'Wikilink without pipe' ); - mw.messages.set( 'jquerymsg-test-version-entrypoints-index-php', '[https://www.mediawiki.org/wiki/Manual:index.php index.php]' ); assert.equal( parser( 'jquerymsg-test-version-entrypoints-index-php' ), - 'index.php', + expectedEntrypoints, 'External link' ); @@ -304,36 +309,74 @@ QUnit.test( 'Links', 6, function ( assert ) { ); }); -// Output for format plain when calling main (mediawiki.js) API. -// We're testing here to ensure our monkey-patching of mw.Message.prototype.parser doesn't -// cause breakage. +// Tests that {{-transformation vs. general parsing are done as requested +QUnit.test( 'Curly brace transformation', 14, function ( assert ) { + var formatText, formatParse, oldUserLang; + + oldUserLang = mw.config.get( 'wgUserLanguage' ) ; + + formatText= mw.jqueryMsg.getMessageFunction( { + format: 'text' + } ); + + formatParse = mw.jqueryMsg.getMessageFunction( { + format: 'parse' + } ); + + // When the expected result is the same in both modes + function assertBothModes( parserArguments, expectedResult, assertMessage) { + assert.equal( formatText.apply( null, parserArguments ), expectedResult, assertMessage + ' when format is \'text\'' ); + assert.equal( formatParse.apply( null, parserArguments ), expectedResult, assertMessage + ' when format is \'parse\'' ); + } + + assertBothModes( ['gender-msg', 'Bob', 'male'], 'Bob: blue', 'gender is resolved' ); + + assertBothModes( ['plural-msg', 5], 'Found 5 items', 'plural is resolved' ); + + assertBothModes( ['grammar-msg'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'grammar is resolved' ); + + mw.config.set( 'wgUserLanguage', 'en' ) ; + assertBothModes( ['formatnum-msg', '987654321.654321'], '987654321.654321', 'formatnum is resolved' ); -// Some of the tests use mw.msg, while others have mw.message(...).plain(). These two -// syntaxes should have identical behavior. -QUnit.test( 'Plain', 4, function ( assert ) { + // Test non-{{ wikitext, where behavior differs + + // Wikilink assert.equal( - mw.message( 'jquerymsg-test-pagetriage-del-talk-page-notify-summary' ).plain(), - 'Notifying author of deletion nomination for [[$1]]', - 'Square brackets in plain with no parameters' + formatText( 'jquerymsg-test-statistics-users' ), + mw.messages.get( 'jquerymsg-test-statistics-users' ), + 'Internal link message unchanged when format is \'text\'' ); - assert.equal( - mw.msg( 'jquerymsg-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName ), - 'Notifying author of deletion nomination for [[' + specialCharactersPageName + ']]', - 'Square brackets in plain with one parameter' + formatParse( 'jquerymsg-test-statistics-users' ), + expectedListUsers, + 'Internal link message parsed when format is \'parse\'' ); + // External link + assert.equal( + formatText( 'jquerymsg-test-version-entrypoints-index-php' ), + mw.messages.get( 'jquerymsg-test-version-entrypoints-index-php' ), + 'External link message unchanged when format is \'text\'' + ); assert.equal( - mw.msg( 'jquerymsg-test-categorytree-collapse-bullet' ), - mw.messages.get( 'jquerymsg-test-categorytree-collapse-bullet' ), - 'Message with single square brackets is not changed' + formatParse( 'jquerymsg-test-version-entrypoints-index-php' ), + expectedEntrypoints, + 'External link message processed when format is \'parse\'' ); + // External link with parameter + assert.equal( + formatText( 'external-link-replace', 'http://example.com' ), + 'Foo [http://example.com bar]', + 'External link message only substitutes parameter when format is \'text\'' + ); assert.equal( - mw.message( 'jquerymsg-test-wikieditor-toolbar-help-content-signature-result' ).plain(), - mw.messages.get( 'jquerymsg-test-wikieditor-toolbar-help-content-signature-result' ), - 'HTML message with curly braces is not changed' + formatParse( 'external-link-replace', 'http://example.com' ), + 'Foo bar', + 'External link message processed when format is \'parse\'' ); + + mw.config.set( 'wgUserLanguage', oldUserLang ); } ); QUnit.test( 'Int', 4, function ( assert ) { @@ -357,8 +400,6 @@ QUnit.test( 'Int', 4, function ( assert ) { 'Link with nested message' ); - mw.messages.set( 'portal-url', 'Project:Community portal' ); - mw.messages.set( 'see-portal-url', '{{Int:portal-url}} is an important community page.' ); assert.equal( parser( 'see-portal-url' ), 'Project:Community portal is an important community page.', @@ -385,7 +426,7 @@ QUnit.test( 'Int', 4, function ( assert ) { // Tests that getMessageFunction is used for non-plain messages with curly braces or // square brackets, but not otherwise. -QUnit.test( 'mw.msg()', 22, function ( assert ) { +QUnit.test( 'mw.Message.prototype.parser monkey-patch', 22, function ( assert ) { var oldGMF, outerCalled, innerCalled; mw.messages.set( { @@ -487,7 +528,6 @@ formatnumTests = [ ]; QUnit.test( 'formatnum', formatnumTests.length, function ( assert ) { - mw.messages.set( 'formatnum-msg', '{{formatnum:$1}}' ); mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' ); $.each( formatnumTests, function ( i, test ) { QUnit.stop(); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js index dc7bd0a6a4..ec061a6cb7 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js @@ -1,6 +1,31 @@ ( function ( mw, $ ) { -QUnit.module( 'mediawiki', QUnit.newMwEnvironment() ); +var specialCharactersPageName; + + +// Since QUnitTestResources.php loads both mediawiki and mediawiki.jqueryMsg as +// dependencies, this only tests the monkey-patched behavior with the two of them combined. + +// See mediawiki.jqueryMsg.test.js for unit tests for jqueryMsg-specific functionality. + +QUnit.module( 'mediawiki', QUnit.newMwEnvironment( { + setup: function () { + // Messages used in multiple tests + mw.messages.set( { + 'other-message': 'Other Message', + 'mediawiki-test-pagetriage-del-talk-page-notify-summary': 'Notifying author of deletion nomination for [[$1]]', + 'gender-plural-msg': '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome', + 'grammar-msg': 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}', + 'formatnum-msg': '{{formatnum:$1}}', + 'int-msg': 'Some {{int:other-message}}' + } ); + + // For formatnum tests + mw.config.set( 'wgUserLanguage', 'en' ); + + specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?'; + } +} ) ); QUnit.test( 'Initial check', 8, function ( assert ) { assert.ok( window.jQuery, 'jQuery defined' ); @@ -77,8 +102,17 @@ QUnit.test( 'mw.config', 1, function ( assert ) { assert.ok( mw.config instanceof mw.Map, 'mw.config instance of mw.Map' ); }); -QUnit.test( 'mw.message & mw.messages', 20, function ( assert ) { - var goodbye, hello, pluralMessage; +QUnit.test( 'mw.message & mw.messages', 54, function ( assert ) { + var goodbye, hello; + + // Convenience method for asserting the same result for multiple formats + function assertMultipleFormats( messageArguments, formats, expectedResult, assertMessage) { + var len = formats.length, format, i; + for ( i = 0; i < len; i++ ) { + format = formats[i]; + assert.equal( mw.message.apply( null, messageArguments )[format](), expectedResult, assertMessage + ' when format is ' + format); + } + } assert.ok( mw.messages, 'messages defined' ); assert.ok( mw.messages instanceof mw.Map, 'mw.messages instance of mw.Map' ); @@ -86,7 +120,9 @@ QUnit.test( 'mw.message & mw.messages', 20, function ( assert ) { hello = mw.message( 'hello' ); - assert.equal( hello.format, 'plain', 'Message property "format" defaults to "plain"' ); + // https://bugzilla.wikimedia.org/show_bug.cgi?id=44459 + assert.equal( hello.format, 'text', 'Message property "format" defaults to "text"' ); + assert.strictEqual( hello.map, mw.messages, 'Message property "map" defaults to the global instance in mw.messages' ); assert.equal( hello.key, 'hello', 'Message property "key" (currect key)' ); assert.deepEqual( hello.parameters, [], 'Message property "parameters" defaults to an empty array' ); @@ -100,29 +136,62 @@ QUnit.test( 'mw.message & mw.messages', 20, function ( assert ) { assert.equal( hello.escaped(), 'Hello <b>awesome</b> world', 'Message.escaped returns the escaped message' ); assert.equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' ); + assert.ok( mw.messages.set( 'escaped-with-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ) ); + assert.equal( mw.message( 'escaped-with-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' ); + + assert.ok( mw.messages.set( 'escaped-with-square-brackets', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ) ); + assert.equal( mw.message( 'escaped-with-square-brackets' ).escaped(), 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]', 'Escaped format works correctly for square bracket message' ); + hello.parse(); assert.equal( hello.format, 'parse', 'Message.parse correctly updated the "format" property' ); hello.plain(); assert.equal( hello.format, 'plain', 'Message.plain correctly updated the "format" property' ); + hello.text(); + assert.equal( hello.format, 'text', 'Message.text correctly updated the "format" property' ); + assert.strictEqual( hello.exists(), true, 'Message.exists returns true for existing messages' ); goodbye = mw.message( 'goodbye' ); assert.strictEqual( goodbye.exists(), false, 'Message.exists returns false for nonexistent messages' ); - assert.equal( goodbye.plain(), '', 'Message.toString returns plain if format is "plain" and key does not exist' ); + assertMultipleFormats( ['goodbye'], ['plain', 'text'], '', 'Message.toString returns if key does not exist' ); // bug 30684 - assert.equal( goodbye.escaped(), '<goodbye>', 'Message.toString returns properly escaped <key> if format is "escaped" and key does not exist' ); + assertMultipleFormats( ['goodbye'], ['parse', 'escaped'], '<goodbye>', 'Message.toString returns properly escaped <key> if key does not exist' ); + + assert.ok( mw.messages.set( 'plural-test-msg', 'There {{PLURAL:$1|is|are}} $1 {{PLURAL:$1|result|results}}' ), 'mw.messages.set: Register' ); + assertMultipleFormats( ['plural-test-msg', 6], ['text', 'parse', 'escaped'], 'There are 6 results', 'plural get resolved' ); + assert.equal( mw.message( 'plural-test-msg', 6 ).plain(), 'There {{PLURAL:6|is|are}} 6 {{PLURAL:6|result|results}}', 'Parameter is substituted but plural is not resolved in plain' ); + + assertMultipleFormats( ['mediawiki-test-pagetriage-del-talk-page-notify-summary'], ['plain', 'text'], mw.messages.get( 'mediawiki-test-pagetriage-del-talk-page-notify-summary' ), 'Double square brackets with no parameters unchanged' ); + + assertMultipleFormats( ['mediawiki-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName], ['plain', 'text'], 'Notifying author of deletion nomination for [[' + specialCharactersPageName + ']]', 'Double square brackets with one parameter' ); + + assert.equal( mw.message( 'mediawiki-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName ).escaped(), 'Notifying author of deletion nomination for [[' + mw.html.escape( specialCharactersPageName ) + ']]', 'Double square brackets with one parameter, when escaped' ); + + + assert.ok( mw.messages.set( 'mediawiki-test-categorytree-collapse-bullet', '[−]' ), 'mw.messages.set: Register' ); + assert.equal( mw.message( 'mediawiki-test-categorytree-collapse-bullet' ).plain(), mw.messages.get( 'mediawiki-test-categorytree-collapse-bullet' ), 'Single square brackets unchanged in plain mode' ); - assert.ok( mw.messages.set( 'pluraltestmsg', 'There {{PLURAL:$1|is|are}} $1 {{PLURAL:$1|result|results}}' ), 'mw.messages.set: Register' ); - pluralMessage = mw.message( 'pluraltestmsg' , 6 ); - assert.equal( pluralMessage.plain(), 'There are 6 results', 'plural get resolved when format is plain' ); - assert.equal( pluralMessage.parse(), 'There are 6 results', 'plural get resolved when format is parse' ); + assert.ok( mw.messages.set( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result', 'Username (talk)' ) ); + assert.equal( mw.message( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ).plain(), mw.messages.get( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ), 'HTML message with curly braces is not changed in plain mode' ); + assertMultipleFormats( ['gender-plural-msg', 'male', 1], ['text', 'parse', 'escaped'], 'he is awesome', 'Gender and plural are resolved' ); + assert.equal( mw.message( 'gender-plural-msg', 'male', 1 ).plain(), '{{GENDER:male|he|she|they}} {{PLURAL:1|is|are}} awesome', 'Parameters are substituted, but gender and plural are not resolved in plain mode' ); + + assert.equal( mw.message( 'grammar-msg' ).plain(), mw.messages.get( 'grammar-msg' ), 'Grammar is not resolved in plain mode' ); + assertMultipleFormats( ['grammar-msg'], ['text', 'parse'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar is resolved' ); + assert.equal( mw.message( 'grammar-msg' ).escaped(), 'Przeszukaj ' + mw.html.escape( mw.config.get( 'wgSiteName' ) ), 'Grammar is resolved in escaped mode' ); + + assertMultipleFormats( ['formatnum-msg', '987654321.654321'], ['text', 'parse', 'escaped'], '987654321.654321', 'formatnum is resolved' ); + assert.equal( mw.message( 'formatnum-msg' ).plain(), mw.messages.get( 'formatnum-msg' ), 'formatnum is not resolved in plain mode' ); + + assertMultipleFormats( ['int-msg'], ['text', 'parse', 'escaped'], 'Some Other Message', 'int is resolved' ); + assert.equal( mw.message( 'int-msg' ).plain(), mw.messages.get( 'int-msg' ), 'int is not resolved in plain mode' ); }); -QUnit.test( 'mw.msg', 11, function ( assert ) { +QUnit.test( 'mw.msg', 14, function ( assert ) { assert.ok( mw.messages.set( 'hello', 'Hello awesome world' ), 'mw.messages.set: Register' ); assert.equal( mw.msg( 'hello' ), 'Hello awesome world', 'Gets message with default options (existing message)' ); assert.equal( mw.msg( 'goodbye' ), '', 'Gets message with default options (nonexistent message)' ); @@ -132,11 +201,17 @@ QUnit.test( 'mw.msg', 11, function ( assert ) { assert.equal( mw.msg( 'plural-item', 0 ), 'Found 0 items', 'Apply plural for count 0' ); assert.equal( mw.msg( 'plural-item', 1 ), 'Found 1 item', 'Apply plural for count 1' ); - assert.ok( mw.messages.set('gender-plural-msg' , '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome' ) ); + assert.equal( mw.msg( 'mediawiki-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName ), 'Notifying author of deletion nomination for [[' + specialCharactersPageName + ']]', 'Double square brackets in mw.msg one parameter' ); + assert.equal( mw.msg( 'gender-plural-msg', 'male', 1 ), 'he is awesome', 'Gender test for male, plural count 1' ); assert.equal( mw.msg( 'gender-plural-msg', 'female', '1' ), 'she is awesome', 'Gender test for female, plural count 1' ); assert.equal( mw.msg( 'gender-plural-msg', 'unknown', 10 ), 'they are awesome', 'Gender test for neutral, plural count 10' ); + assert.equal( mw.msg( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName'), 'Grammar is resolved' ); + + assert.equal( mw.msg( 'formatnum-msg', '987654321.654321' ), '987654321.654321', 'formatnum is resolved' ); + + assert.equal( mw.msg( 'int-msg' ), 'Some Other Message', 'int is resolved' ); }); /**