(bug 43498) (bug 43574) Two wikilink types and {{int:}}.
authorMatthew Flaschen <mflaschen@wikimedia.org>
Sat, 29 Dec 2012 02:39:12 +0000 (21:39 -0500)
committerMatthew Flaschen <mflaschen@wikimedia.org>
Tue, 15 Jan 2013 00:39:59 +0000 (19:39 -0500)
* Bug 43498 - Implement two wikilink types, with tests. Also add test
for external link, which was already implemented in the
main code.

* Bug 43574 - Support {{int:}} message includes in jqueryMsg, with tests.
* Add tests for when the old mw.msg should be called, vs. getMessageFunction.
Change-Id: I7a748eb99259103f491ff5a5aa0bf223fc0b76c4

resources/mediawiki/mediawiki.jqueryMsg.js
resources/mediawiki/mediawiki.jqueryMsg.peg
tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js

index 6e2d3b4..c599f4b 100644 (file)
                                regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, backslash, anyCharacter,
                                escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
                                whitespace, dollar, digits,
-                               openExtlink, closeExtlink, openLink, closeLink, templateName, pipe, colon,
+                               openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openLink, closeLink, templateName, pipe, colon,
                                templateContents, openTemplate, closeTemplate,
                                nonWhitespaceExpression, paramExpression, expression, result;
 
                                 var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
                                 return result === null ? null : result.join('');
                        }
+
+                       // Used for wikilink page names.  Like literalWithoutBar, but
+                       // without allowing escapes.
+                       function unescapedLiteralWithoutBar() {
+                               var result = nOrMore( 1, regularLiteralWithoutBar )();
+                               return result === null ? null : result.join('');
+                       }
+
                        function literal() {
                                 var result = nOrMore( 1, escapedOrRegularLiteral )();
                                 return result === null ? null : result.join('');
                        }
                        openLink = makeStringParser( '[[' );
                        closeLink = makeStringParser( ']]' );
+                       pipe = makeStringParser( '|' );
+
+                       function template() {
+                               var result = sequence( [
+                                       openTemplate,
+                                       templateContents,
+                                       closeTemplate
+                               ] );
+                               return result === null ? null : result[1];
+                       }
+
+                       wikilinkPage = choice( [
+                               unescapedLiteralWithoutBar,
+                               template
+                       ] );
+
+                       function pipedWikilink() {
+                               var result = sequence( [
+                                       wikilinkPage,
+                                       pipe,
+                                       expression
+                               ] );
+                               return result === null ? null : [ result[0], result[2] ];
+                       }
+
+                       wikilinkContents = choice( [
+                               pipedWikilink,
+                               wikilinkPage // unpiped link
+                       ] );
+
                        function link() {
-                               var result, parsedResult;
+                               var result, parsedResult, parsedLinkContents;
                                result = null;
+
                                parsedResult = sequence( [
                                        openLink,
-                                       expression,
+                                       wikilinkContents,
                                        closeLink
                                ] );
                                if ( parsedResult !== null ) {
-                                        result = [ 'WLINK', parsedResult[1] ];
+                                       parsedLinkContents = parsedResult[1];
+                                       result = [ 'WLINK' ].concat( parsedLinkContents );
                                }
                                return result;
                        }
                                // use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
                                return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[0];
                        }
-                       pipe = makeStringParser( '|' );
+
                        function templateWithReplacement() {
                                var result = sequence( [
                                        templateName,
                        ] );
                        openTemplate = makeStringParser('{{');
                        closeTemplate = makeStringParser('}}');
-                       function template() {
-                               var result = sequence( [
-                                       openTemplate,
-                                       templateContents,
-                                       closeTemplate
-                               ] );
-                               return result === null ? null : result[1];
-                       }
                        nonWhitespaceExpression = choice( [
                                template,
                                link,
                                replacement,
                                literalWithoutBar
                        ] );
+
                        expression = choice( [
                                template,
                                link,
                                replacement,
                                literal
                        ] );
+
                        function start() {
                                var result = nOrMore( 0, expression )();
                                if ( result === null ) {
 
                /**
                 * Transform wiki-link
-                * TODO unimplemented
+                *
+                * TODO:
+                * It only handles basic cases, either no pipe, or a pipe with an explicit
+                * anchor.
+                *
+                * It does not attempt to handle features like the pipe trick.
+                * However, the pipe trick should usually not be present in wikitext retrieved
+                * from the server, since the replacement is done at save time.
+                * It may, though, if the wikitext appears in extension-controlled content.
+                *
                 * @param nodes
                 */
-               wlink: function () {
-                       return 'unimplemented';
+               wlink: function ( nodes ) {
+                       var page, anchor, url;
+
+                       page = nodes[0];
+                       url = mw.util.wikiGetlink( page );
+
+                       // [[Some Page]] or [[Namespace:Some Page]]
+                       if ( nodes.length === 1 ) {
+                               anchor = page;
+                       }
+
+                       /*
+                        * [[Some Page|anchor text]] or
+                        * [[Namespace:Some Page|anchor]
+                        */
+                       else {
+                               anchor = nodes[1];
+                       }
+
+                       return $( '<a />' ).attr( {
+                               title: page,
+                               href: url
+                       } ).text( anchor );
                },
 
                /**
                        var form = nodes[0],
                                word = nodes[1];
                        return word && form && this.language.convertGrammar( word, form );
+               },
+
+               /**
+                * Tranform parsed structure into a int: (interface language) message include
+                * Invoked by putting {{MediaWiki:othermessage}} into a message
+                * @param {Array} of nodes
+                * @return {string} Other message
+                */
+               int: function ( nodes ) {
+                       return mw.jqueryMsg.getMessageFunction()( nodes[0].toLowerCase() );
                }
        };
        // Deprecated! don't rely on gM existing.
                // 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.
                // Do not use mw.jqueryMsg unless required
-               if ( this.map.get( this.key ).indexOf( '{{' ) < 0 ) {
+               if ( !/\{\{|\[/.test(this.map.get( this.key ) ) ) {
                        // Fall back to mw.msg's simple parser
                        return oldParser.apply( this );
                }
index e059ed1..7879d6f 100644 (file)
@@ -37,6 +37,7 @@ templateParam
 templateName
   = tn:[A-Za-z_]+ { return tn.join('').toUpperCase() }
 
+/* TODO: Update to reflect separate piped and unpiped handling */
 link
   = "[[" w:expression "]]" { return [ 'WLINK', w ]; }
 
index bce7bd7..29e94a5 100644 (file)
@@ -1,16 +1,27 @@
 ( function ( mw, $ ) {
 
+var mwLanguageCache = {}, oldGetOuterHtml;
+
 QUnit.module( 'mediawiki.jqueryMsg', QUnit.newMwEnvironment( {
        setup: function () {
                this.orgMwLangauge = mw.language;
                mw.language = $.extend( true, {}, this.orgMwLangauge );
+               oldGetOuterHtml = $.fn.getOuterHtml;
+               $.fn.getOuterHtml = function () {
+                       var $div = $( '<div>' ), html;
+                       $div.append( $( this ).eq( 0 ).clone() );
+                       html = $div.html();
+                       $div.empty();
+                       $div = undefined;
+                       return html;
+               };
        },
        teardown: function () {
                mw.language = this.orgMwLangauge;
+               $.fn.getOuterHtml = oldGetOuterHtml;
        }
 }) );
 
-var mwLanguageCache = {};
 function getMwLanguage( langCode, cb ) {
        if ( mwLanguageCache[langCode] !== undefined ) {
                mwLanguageCache[langCode].add( cb );
@@ -154,4 +165,168 @@ QUnit.test( 'Output matches PHP parser', mw.libs.phpParserData.tests.length, fun
        } );
 });
 
+QUnit.test( 'Links', 6, function ( assert ) {
+       var parser = mw.jqueryMsg.getMessageFunction(),
+               expectedListUsers,
+               expectedDisambiguationsText,
+               expectedMultipleBars,
+               expectedSpecialCharacters,
+               specialCharactersPageName;
+
+       /*
+        The below three are all identical to or based on real messages.  For disambiguations-text,
+        the bold was removed because it is not yet implemented.
+       */
+
+       mw.messages.set( 'statistics-users', '注册[[Special:ListUsers|用户]]' );
+
+       expectedListUsers = '注册' + $( '<a>' ).attr( {
+               title: 'Special:ListUsers',
+               href: mw.util.wikiGetlink( 'Special:ListUsers' )
+       } ).text( '用户' ).getOuterHtml();
+
+       assert.equal(
+               parser( 'statistics-users' ),
+               expectedListUsers,
+               'Piped wikilink'
+       );
+
+       expectedDisambiguationsText = 'The following pages contain at least one link to a disambiguation page.\nThey may have to link to a more appropriate page instead.<br>\nA page is treated as a disambiguation page if it uses a template that is linked from ' +
+               $( '<a>' ).attr( {
+                       title: 'MediaWiki:Disambiguationspage',
+                       href: mw.util.wikiGetlink( 'MediaWiki:Disambiguationspage' )
+               } ).text( 'MediaWiki:Disambiguationspage' ).getOuterHtml() + '.';
+       mw.messages.set( 'disambiguations-text', 'The following pages contain at least one link to a disambiguation page.\nThey may have to link to a more appropriate page instead.<br />\nA page is treated as a disambiguation page if it uses a template that is linked from [[MediaWiki:Disambiguationspage]].' );
+       assert.equal(
+               parser( 'disambiguations-text' ),
+               expectedDisambiguationsText,
+               'Wikilink without pipe'
+       );
+
+       mw.messages.set( 'version-entrypoints-index-php', '[https://www.mediawiki.org/wiki/Manual:index.php index.php]' );
+       assert.equal(
+               parser( 'version-entrypoints-index-php' ),
+               '<a href="https://www.mediawiki.org/wiki/Manual:index.php">index.php</a>',
+               'External link'
+       );
+
+       // Pipe trick is not supported currently, but should not parse as text either.
+       mw.messages.set( 'pipe-trick', '[[Tampa, Florida|]]' );
+       assert.equal(
+               parser( 'pipe-trick' ),
+               'Error: Parse error at position 0 in input: [[Tampa, Florida|]]',
+               'Pipe trick should return error string.'
+       );
+
+       expectedMultipleBars = $( '<a>' ).attr( {
+               title: 'Main Page',
+               href: mw.util.wikiGetlink( 'Main Page' )
+       } ).text( 'Main|Page' ).getOuterHtml();
+       mw.messages.set( 'multiple-bars', '[[Main Page|Main|Page]]' );
+       assert.equal(
+               parser( 'multiple-bars' ),
+               expectedMultipleBars,
+               'Bar in anchor'
+       );
+
+       specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?';
+       expectedSpecialCharacters = $( '<a>' ).attr( {
+               title: specialCharactersPageName,
+               href: mw.util.wikiGetlink( specialCharactersPageName )
+       } ).text( specialCharactersPageName ).getOuterHtml();
+
+       mw.messages.set( 'special-characters', '[[' + specialCharactersPageName + ']]' );
+       assert.equal(
+               parser( 'special-characters' ),
+               expectedSpecialCharacters,
+               'Special characters'
+       );
+});
+
+QUnit.test( 'Int magic word', 4, function ( assert ) {
+       var parser = mw.jqueryMsg.getMessageFunction(),
+           newarticletextSource = 'You have followed a link to a page that does not exist yet. To create the page, start typing in the box below (see the [[{{Int:Helppage}}|help page]] for more info). If you are here by mistake, click your browser\'s back button.',
+               expectedNewarticletext;
+
+       mw.messages.set( 'helppage', 'Help:Contents' );
+
+       expectedNewarticletext = 'You have followed a link to a page that does not exist yet. To create the page, start typing in the box below (see the ' +
+               $( '<a>' ).attr( {
+                       title: mw.msg( 'helppage' ),
+                       href: mw.util.wikiGetlink( mw.msg( 'helppage' ) )
+               } ).text( 'help page' ).getOuterHtml() + ' for more info). If you are here by mistake, click your browser\'s back button.';
+
+       mw.messages.set( 'newarticletext', newarticletextSource );
+
+       assert.equal(
+               parser( 'newarticletext' ),
+               expectedNewarticletext,
+               '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.',
+               'Nested message'
+       );
+
+       mw.messages.set( 'newarticletext-lowercase',
+               newarticletextSource.replace( 'Int:Helppage', 'int:helppage' ) );
+
+       assert.equal(
+               parser( 'newarticletext-lowercase' ),
+               expectedNewarticletext,
+               'Link with nested message, lowercase include'
+       );
+
+       mw.messages.set( 'uses-missing-int', '{{int:doesnt-exist}}' );
+
+       assert.equal(
+               parser( 'uses-missing-int' ),
+               '[doesnt-exist]',
+               'int: where nested message does not exist'
+       );
+});
+
+// Tests that getMessageFunction is used for messages with curly braces or square brackets,
+// but not otherwise.
+QUnit.test( 'Calls to mw.msg', 8, function ( assert ) {
+       // Should be
+       var map, oldGMF, outerCalled, innerCalled;
+
+       map = new mw.Map();
+       map.set( {
+               'curly-brace': '{{int:message}}',
+               'single-square-bracket': '[https://www.mediawiki.org/ MediaWiki]',
+               'double-square-bracket': '[[Some page]]',
+               'regular': 'Other message'
+       } );
+
+       oldGMF = mw.jqueryMsg.getMessageFunction;
+
+       mw.jqueryMsg.getMessageFunction = function() {
+               outerCalled = true;
+               return function() {
+                       innerCalled = true;
+               };
+       };
+
+       function verifyGetMessageFunction( key, shouldCall ) {
+               outerCalled = false;
+               innerCalled = false;
+               ( new mw.Message( map, key ) ).parser();
+               assert.strictEqual( outerCalled, shouldCall, 'Outer function called for ' + key );
+               assert.strictEqual( innerCalled, shouldCall, 'Inner function called for ' + key );
+       }
+
+       verifyGetMessageFunction( 'curly-brace', true );
+       verifyGetMessageFunction( 'single-square-bracket', true );
+       verifyGetMessageFunction( 'double-square-bracket', true );
+       verifyGetMessageFunction( 'regular', false );
+
+       mw.jqueryMsg.getMessageFunction = oldGMF;
+} );
+
 }( mediaWiki, jQuery ) );