Implement extension registration from an extension.json file
[mediawiki.git] / resources / src / mediawiki / mediawiki.jqueryMsg.js
blob3eaa6d2cd16780369b132eb80ce74ab92b34a68a
1 /*!
2 * Experimental advanced wikitext parser-emitter.
3 * See: https://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs
5 * @author neilk@wikimedia.org
6 * @author mflaschen@wikimedia.org
7 */
8 ( function ( mw, $ ) {
9         /**
10          * @class mw.jqueryMsg
11          * @singleton
12          */
14         var oldParser,
15                 slice = Array.prototype.slice,
16                 parserDefaults = {
17                         magic: {
18                                 'SITENAME': mw.config.get( 'wgSiteName' )
19                         },
20                         // This is a whitelist based on, but simpler than, Sanitizer.php.
21                         // Self-closing tags are not currently supported.
22                         allowedHtmlElements: [
23                                 'b',
24                                 'i'
25                         ],
26                         // Key tag name, value allowed attributes for that tag.
27                         // See Sanitizer::setupAttributeWhitelist
28                         allowedHtmlCommonAttributes: [
29                                 // HTML
30                                 'id',
31                                 'class',
32                                 'style',
33                                 'lang',
34                                 'dir',
35                                 'title',
37                                 // WAI-ARIA
38                                 'role'
39                         ],
41                         // Attributes allowed for specific elements.
42                         // Key is element name in lower case
43                         // Value is array of allowed attributes for that element
44                         allowedHtmlAttributesByElement: {},
45                         messages: mw.messages,
46                         language: mw.language,
48                         // Same meaning as in mediawiki.js.
49                         //
50                         // Only 'text', 'parse', and 'escaped' are supported, and the
51                         // actual escaping for 'escaped' is done by other code (generally
52                         // through mediawiki.js).
53                         //
54                         // However, note that this default only
55                         // applies to direct calls to jqueryMsg. The default for mediawiki.js itself
56                         // is 'text', including when it uses jqueryMsg.
57                         format: 'parse'
59                 };
61         /**
62          * Wrapper around jQuery append that converts all non-objects to TextNode so append will not
63          * convert what it detects as an htmlString to an element.
64          *
65          * Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is.
66          *
67          * @private
68          * @param {jQuery} $parent Parent node wrapped by jQuery
69          * @param {Object|string|Array} children What to append, with the same possible types as jQuery
70          * @return {jQuery} $parent
71          */
72         function appendWithoutParsing( $parent, children ) {
73                 var i, len;
75                 if ( !$.isArray( children ) ) {
76                         children = [children];
77                 }
79                 for ( i = 0, len = children.length; i < len; i++ ) {
80                         if ( typeof children[i] !== 'object' ) {
81                                 children[i] = document.createTextNode( children[i] );
82                         }
83                 }
85                 return $parent.append( children );
86         }
88         /**
89          * Decodes the main HTML entities, those encoded by mw.html.escape.
90          *
91          * @private
92          * @param {string} encoded Encoded string
93          * @return {string} String with those entities decoded
94          */
95         function decodePrimaryHtmlEntities( encoded ) {
96                 return encoded
97                         .replace( /&#039;/g, '\'' )
98                         .replace( /&quot;/g, '"' )
99                         .replace( /&lt;/g, '<' )
100                         .replace( /&gt;/g, '>' )
101                         .replace( /&amp;/g, '&' );
102         }
104         /**
105          * Given parser options, return a function that parses a key and replacements, returning jQuery object
106          *
107          * Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes.
108          * If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into
109          * the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it.
110          * @private
111          * @param {Object} options Parser options
112          * @return {Function}
113          * @return {Array} return.args First element is the key, replacements may be in array in 2nd element, or remaining elements.
114          * @return {jQuery} return.return
115          */
116         function getFailableParserFn( options ) {
117                 var parser = new mw.jqueryMsg.parser( options );
119                 return function ( args ) {
120                         var fallback,
121                                 key = args[0],
122                                 argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 );
123                         try {
124                                 return parser.parse( key, argsArray );
125                         } catch ( e ) {
126                                 fallback = parser.settings.messages.get( key );
127                                 mw.log.warn( 'mediawiki.jqueryMsg: ' + key + ': ' + e.message );
128                                 return $( '<span>' ).text( fallback );
129                         }
130                 };
131         }
133         mw.jqueryMsg = {};
135         /**
136          * Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements).
137          * e.g.
138          *
139          *       window.gM = mediaWiki.parser.getMessageFunction( options );
140          *       $( 'p#headline' ).html( gM( 'hello-user', username ) );
141          *
142          * Like the old gM() function this returns only strings, so it destroys any bindings. If you want to preserve bindings use the
143          * jQuery plugin version instead. This is only included for backwards compatibility with gM().
144          *
145          * N.B. replacements are variadic arguments or an array in second parameter. In other words:
146          *    somefunction( a, b, c, d )
147          * is equivalent to
148          *    somefunction( a, [b, c, d] )
149          *
150          * @param {Object} options parser options
151          * @return {Function} Function suitable for assigning to window.gM
152          * @return {string} return.key Message key.
153          * @return {Array|Mixed} return.replacements Optional variable replacements (variadically or an array).
154          * @return {string} return.return Rendered HTML.
155          */
156         mw.jqueryMsg.getMessageFunction = function ( options ) {
157                 var failableParserFn = getFailableParserFn( options ),
158                         format;
160                 if ( options && options.format !== undefined ) {
161                         format = options.format;
162                 } else {
163                         format = parserDefaults.format;
164                 }
166                 return function () {
167                         var failableResult = failableParserFn( arguments );
168                         if ( format === 'text' || format === 'escaped' ) {
169                                 return failableResult.text();
170                         } else {
171                                 return failableResult.html();
172                         }
173                 };
174         };
176         /**
177          * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to
178          * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.
179          * e.g.
180          *
181          *        $.fn.msg = mediaWiki.parser.getJqueryPlugin( options );
182          *        var userlink = $( '<a>' ).click( function () { alert( "hello!!" ) } );
183          *        $( 'p#headline' ).msg( 'hello-user', userlink );
184          *
185          * N.B. replacements are variadic arguments or an array in second parameter. In other words:
186          *    somefunction( a, b, c, d )
187          * is equivalent to
188          *    somefunction( a, [b, c, d] )
189          *
190          * We append to 'this', which in a jQuery plugin context will be the selected elements.
191          *
192          * @param {Object} options Parser options
193          * @return {Function} Function suitable for assigning to jQuery plugin, such as jQuery#msg
194          * @return {string} return.key Message key.
195          * @return {Array|Mixed} return.replacements Optional variable replacements (variadically or an array).
196          * @return {jQuery} return.return
197          */
198         mw.jqueryMsg.getPlugin = function ( options ) {
199                 var failableParserFn = getFailableParserFn( options );
201                 return function () {
202                         var $target = this.empty();
203                         // TODO: Simply appendWithoutParsing( $target, failableParserFn( arguments ).contents() )
204                         // or Simply appendWithoutParsing( $target, failableParserFn( arguments ) )
205                         $.each( failableParserFn( arguments ).contents(), function ( i, node ) {
206                                 appendWithoutParsing( $target, node );
207                         } );
208                         return $target;
209                 };
210         };
212         /**
213          * The parser itself.
214          * Describes an object, whose primary duty is to .parse() message keys.
215          *
216          * @class
217          * @private
218          * @param {Object} options
219          */
220         mw.jqueryMsg.parser = function ( options ) {
221                 this.settings = $.extend( {}, parserDefaults, options );
222                 this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' );
224                 this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic );
225         };
227         mw.jqueryMsg.parser.prototype = {
228                 /**
229                  * Cache mapping MediaWiki message keys and the value onlyCurlyBraceTransform, to the AST of the message.
230                  *
231                  * In most cases, the message is a string so this is identical.
232                  * (This is why we would like to move this functionality server-side).
233                  *
234                  * The two parts of the key are separated by colon.  For example:
235                  *
236                  *     "message-key:true": ast
237                  *
238                  * if they key is "message-key" and onlyCurlyBraceTransform is true.
239                  *
240                  * This cache is shared by all instances of mw.jqueryMsg.parser.
241                  *
242                  * NOTE: We promise, it's static - when you create this empty object
243                  * in the prototype, each new instance of the class gets a reference
244                  * to the same object.
245                  *
246                  * @static
247                  * @property {Object}
248                  */
249                 astCache: {},
251                 /**
252                  * Where the magic happens.
253                  * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery
254                  * If an error is thrown, returns original key, and logs the error
255                  * @param {string} key Message key.
256                  * @param {Array} replacements Variable replacements for $1, $2... $n
257                  * @return {jQuery}
258                  */
259                 parse: function ( key, replacements ) {
260                         return this.emitter.emit( this.getAst( key ), replacements );
261                 },
263                 /**
264                  * Fetch the message string associated with a key, return parsed structure. Memoized.
265                  * Note that we pass '[' + key + ']' back for a missing message here.
266                  * @param {string} key
267                  * @return {string|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
268                  */
269                 getAst: function ( key ) {
270                         var cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' ), wikiText;
272                         if ( this.astCache[ cacheKey ] === undefined ) {
273                                 wikiText = this.settings.messages.get( key );
274                                 if ( typeof wikiText !== 'string' ) {
275                                         wikiText = '\\[' + key + '\\]';
276                                 }
277                                 this.astCache[ cacheKey ] = this.wikiTextToAst( wikiText );
278                         }
279                         return this.astCache[ cacheKey ];
280                 },
282                 /**
283                  * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
284                  *
285                  * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
286                  * n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
287                  *
288                  * @param {string} input Message string wikitext
289                  * @throws Error
290                  * @return {Mixed} abstract syntax tree
291                  */
292                 wikiTextToAst: function ( input ) {
293                         var pos, settings = this.settings, concat = Array.prototype.concat,
294                                 regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets,
295                                 doubleQuote, singleQuote, backslash, anyCharacter, asciiAlphabetLiteral,
296                                 escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
297                                 whitespace, dollar, digits, htmlDoubleQuoteAttributeValue, htmlSingleQuoteAttributeValue,
298                                 htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag,
299                                 openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
300                                 templateContents, openTemplate, closeTemplate,
301                                 nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result;
303                         // Indicates current position in input as we parse through it.
304                         // Shared among all parsing functions below.
305                         pos = 0;
307                         // =========================================================
308                         // parsing combinators - could be a library on its own
309                         // =========================================================
311                         /**
312                          * Try parsers until one works, if none work return null
313                          * @private
314                          * @param {Function[]} ps
315                          * @return {string|null}
316                          */
317                         function choice( ps ) {
318                                 return function () {
319                                         var i, result;
320                                         for ( i = 0; i < ps.length; i++ ) {
321                                                 result = ps[i]();
322                                                 if ( result !== null ) {
323                                                         return result;
324                                                 }
325                                         }
326                                         return null;
327                                 };
328                         }
330                         /**
331                          * Try several ps in a row, all must succeed or return null.
332                          * This is the only eager one.
333                          * @private
334                          * @param {Function[]} ps
335                          * @return {string|null}
336                          */
337                         function sequence( ps ) {
338                                 var i, res,
339                                         originalPos = pos,
340                                         result = [];
341                                 for ( i = 0; i < ps.length; i++ ) {
342                                         res = ps[i]();
343                                         if ( res === null ) {
344                                                 pos = originalPos;
345                                                 return null;
346                                         }
347                                         result.push( res );
348                                 }
349                                 return result;
350                         }
352                         /**
353                          * Run the same parser over and over until it fails.
354                          * Must succeed a minimum of n times or return null.
355                          * @private
356                          * @param {number} n
357                          * @param {Function} p
358                          * @return {string|null}
359                          */
360                         function nOrMore( n, p ) {
361                                 return function () {
362                                         var originalPos = pos,
363                                                 result = [],
364                                                 parsed = p();
365                                         while ( parsed !== null ) {
366                                                 result.push( parsed );
367                                                 parsed = p();
368                                         }
369                                         if ( result.length < n ) {
370                                                 pos = originalPos;
371                                                 return null;
372                                         }
373                                         return result;
374                                 };
375                         }
377                         /**
378                          * There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null.
379                          *
380                          * TODO: But using this as a combinator seems to cause problems when combined with #nOrMore().
381                          * May be some scoping issue
382                          *
383                          * @private
384                          * @param {Function} p
385                          * @param {Function} fn
386                          * @return {string|null}
387                          */
388                         function transform( p, fn ) {
389                                 return function () {
390                                         var result = p();
391                                         return result === null ? null : fn( result );
392                                 };
393                         }
395                         /**
396                          * Just make parsers out of simpler JS builtin types
397                          * @private
398                          * @param {string} s
399                          * @return {Function}
400                          * @return {string} return.return
401                          */
402                         function makeStringParser( s ) {
403                                 var len = s.length;
404                                 return function () {
405                                         var result = null;
406                                         if ( input.substr( pos, len ) === s ) {
407                                                 result = s;
408                                                 pos += len;
409                                         }
410                                         return result;
411                                 };
412                         }
414                         /**
415                          * Makes a regex parser, given a RegExp object.
416                          * The regex being passed in should start with a ^ to anchor it to the start
417                          * of the string.
418                          *
419                          * @private
420                          * @param {RegExp} regex anchored regex
421                          * @return {Function} function to parse input based on the regex
422                          */
423                         function makeRegexParser( regex ) {
424                                 return function () {
425                                         var matches = input.slice( pos ).match( regex );
426                                         if ( matches === null ) {
427                                                 return null;
428                                         }
429                                         pos += matches[0].length;
430                                         return matches[0];
431                                 };
432                         }
434                         // ===================================================================
435                         // General patterns above this line -- wikitext specific parsers below
436                         // ===================================================================
438                         // Parsing functions follow. All parsing functions work like this:
439                         // They don't accept any arguments.
440                         // Instead, they just operate non destructively on the string 'input'
441                         // As they can consume parts of the string, they advance the shared variable pos,
442                         // and return tokens (or whatever else they want to return).
443                         // some things are defined as closures and other things as ordinary functions
444                         // converting everything to a closure makes it a lot harder to debug... errors pop up
445                         // but some debuggers can't tell you exactly where they come from. Also the mutually
446                         // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
447                         // This may be because, to save code, memoization was removed
449                         regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ );
450                         regularLiteralWithoutBar = makeRegexParser( /^[^{}\[\]$\\|]/ );
451                         regularLiteralWithoutSpace = makeRegexParser( /^[^{}\[\]$\s]/ );
452                         regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ );
454                         backslash = makeStringParser( '\\' );
455                         doubleQuote = makeStringParser( '"' );
456                         singleQuote = makeStringParser( '\'' );
457                         anyCharacter = makeRegexParser( /^./ );
459                         openHtmlStartTag = makeStringParser( '<' );
460                         optionalForwardSlash = makeRegexParser( /^\/?/ );
461                         openHtmlEndTag = makeStringParser( '</' );
462                         htmlAttributeEquals = makeRegexParser( /^\s*=\s*/ );
463                         closeHtmlTag = makeRegexParser( /^\s*>/ );
465                         function escapedLiteral() {
466                                 var result = sequence( [
467                                         backslash,
468                                         anyCharacter
469                                 ] );
470                                 return result === null ? null : result[1];
471                         }
472                         escapedOrLiteralWithoutSpace = choice( [
473                                 escapedLiteral,
474                                 regularLiteralWithoutSpace
475                         ] );
476                         escapedOrLiteralWithoutBar = choice( [
477                                 escapedLiteral,
478                                 regularLiteralWithoutBar
479                         ] );
480                         escapedOrRegularLiteral = choice( [
481                                 escapedLiteral,
482                                 regularLiteral
483                         ] );
484                         // Used to define "literals" without spaces, in space-delimited situations
485                         function literalWithoutSpace() {
486                                 var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
487                                 return result === null ? null : result.join( '' );
488                         }
489                         // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default
490                         // it is not a literal in the parameter
491                         function literalWithoutBar() {
492                                 var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
493                                 return result === null ? null : result.join( '' );
494                         }
496                         // Used for wikilink page names.  Like literalWithoutBar, but
497                         // without allowing escapes.
498                         function unescapedLiteralWithoutBar() {
499                                 var result = nOrMore( 1, regularLiteralWithoutBar )();
500                                 return result === null ? null : result.join( '' );
501                         }
503                         function literal() {
504                                 var result = nOrMore( 1, escapedOrRegularLiteral )();
505                                 return result === null ? null : result.join( '' );
506                         }
508                         function curlyBraceTransformExpressionLiteral() {
509                                 var result = nOrMore( 1, regularLiteralWithSquareBrackets )();
510                                 return result === null ? null : result.join( '' );
511                         }
513                         asciiAlphabetLiteral = makeRegexParser( /[A-Za-z]+/ );
514                         htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ );
515                         htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ );
517                         whitespace = makeRegexParser( /^\s+/ );
518                         dollar = makeStringParser( '$' );
519                         digits = makeRegexParser( /^\d+/ );
521                         function replacement() {
522                                 var result = sequence( [
523                                         dollar,
524                                         digits
525                                 ] );
526                                 if ( result === null ) {
527                                         return null;
528                                 }
529                                 return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ];
530                         }
531                         openExtlink = makeStringParser( '[' );
532                         closeExtlink = makeStringParser( ']' );
533                         // this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed
534                         function extlink() {
535                                 var result, parsedResult;
536                                 result = null;
537                                 parsedResult = sequence( [
538                                         openExtlink,
539                                         nonWhitespaceExpression,
540                                         whitespace,
541                                         nOrMore( 1, expression ),
542                                         closeExtlink
543                                 ] );
544                                 if ( parsedResult !== null ) {
545                                         result = [ 'EXTLINK', parsedResult[1] ];
546                                         // TODO (mattflaschen, 2013-03-22): Clean this up if possible.
547                                         // It's avoiding CONCAT for single nodes, so they at least doesn't get the htmlEmitter span.
548                                         if ( parsedResult[3].length === 1 ) {
549                                                 result.push( parsedResult[3][0] );
550                                         } else {
551                                                 result.push( ['CONCAT'].concat( parsedResult[3] ) );
552                                         }
553                                 }
554                                 return result;
555                         }
556                         // this is the same as the above extlink, except that the url is being passed on as a parameter
557                         function extLinkParam() {
558                                 var result = sequence( [
559                                         openExtlink,
560                                         dollar,
561                                         digits,
562                                         whitespace,
563                                         expression,
564                                         closeExtlink
565                                 ] );
566                                 if ( result === null ) {
567                                         return null;
568                                 }
569                                 return [ 'EXTLINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ];
570                         }
571                         openWikilink = makeStringParser( '[[' );
572                         closeWikilink = makeStringParser( ']]' );
573                         pipe = makeStringParser( '|' );
575                         function template() {
576                                 var result = sequence( [
577                                         openTemplate,
578                                         templateContents,
579                                         closeTemplate
580                                 ] );
581                                 return result === null ? null : result[1];
582                         }
584                         wikilinkPage = choice( [
585                                 unescapedLiteralWithoutBar,
586                                 template
587                         ] );
589                         function pipedWikilink() {
590                                 var result = sequence( [
591                                         wikilinkPage,
592                                         pipe,
593                                         expression
594                                 ] );
595                                 return result === null ? null : [ result[0], result[2] ];
596                         }
598                         wikilinkContents = choice( [
599                                 pipedWikilink,
600                                 wikilinkPage // unpiped link
601                         ] );
603                         function wikilink() {
604                                 var result, parsedResult, parsedLinkContents;
605                                 result = null;
607                                 parsedResult = sequence( [
608                                         openWikilink,
609                                         wikilinkContents,
610                                         closeWikilink
611                                 ] );
612                                 if ( parsedResult !== null ) {
613                                         parsedLinkContents = parsedResult[1];
614                                         result = [ 'WIKILINK' ].concat( parsedLinkContents );
615                                 }
616                                 return result;
617                         }
619                         // TODO: Support data- if appropriate
620                         function doubleQuotedHtmlAttributeValue() {
621                                 var parsedResult = sequence( [
622                                         doubleQuote,
623                                         htmlDoubleQuoteAttributeValue,
624                                         doubleQuote
625                                 ] );
626                                 return parsedResult === null ? null : parsedResult[1];
627                         }
629                         function singleQuotedHtmlAttributeValue() {
630                                 var parsedResult = sequence( [
631                                         singleQuote,
632                                         htmlSingleQuoteAttributeValue,
633                                         singleQuote
634                                 ] );
635                                 return parsedResult === null ? null : parsedResult[1];
636                         }
638                         function htmlAttribute() {
639                                 var parsedResult = sequence( [
640                                         whitespace,
641                                         asciiAlphabetLiteral,
642                                         htmlAttributeEquals,
643                                         choice( [
644                                                 doubleQuotedHtmlAttributeValue,
645                                                 singleQuotedHtmlAttributeValue
646                                         ] )
647                                 ] );
648                                 return parsedResult === null ? null : [parsedResult[1], parsedResult[3]];
649                         }
651                         /**
652                          * Checks if HTML is allowed
653                          *
654                          * @param {string} startTagName HTML start tag name
655                          * @param {string} endTagName HTML start tag name
656                          * @param {Object} attributes array of consecutive key value pairs,
657                          *  with index 2 * n being a name and 2 * n + 1 the associated value
658                          * @return {boolean} true if this is HTML is allowed, false otherwise
659                          */
660                         function isAllowedHtml( startTagName, endTagName, attributes ) {
661                                 var i, len, attributeName;
663                                 startTagName = startTagName.toLowerCase();
664                                 endTagName = endTagName.toLowerCase();
665                                 if ( startTagName !== endTagName || $.inArray( startTagName, settings.allowedHtmlElements ) === -1 ) {
666                                         return false;
667                                 }
669                                 for ( i = 0, len = attributes.length; i < len; i += 2 ) {
670                                         attributeName = attributes[i];
671                                         if ( $.inArray( attributeName, settings.allowedHtmlCommonAttributes ) === -1 &&
672                                                 $.inArray( attributeName, settings.allowedHtmlAttributesByElement[startTagName] || [] ) === -1 ) {
673                                                 return false;
674                                         }
675                                 }
677                                 return true;
678                         }
680                         function htmlAttributes() {
681                                 var parsedResult = nOrMore( 0, htmlAttribute )();
682                                 // Un-nest attributes array due to structure of jQueryMsg operations (see emit).
683                                 return concat.apply( ['HTMLATTRIBUTES'], parsedResult );
684                         }
686                         // Subset of allowed HTML markup.
687                         // Most elements and many attributes allowed on the server are not supported yet.
688                         function html() {
689                                 var result = null, parsedOpenTagResult, parsedHtmlContents,
690                                         parsedCloseTagResult, wrappedAttributes, attributes,
691                                         startTagName, endTagName, startOpenTagPos, startCloseTagPos,
692                                         endOpenTagPos, endCloseTagPos;
694                                 // Break into three sequence calls.  That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match.
695                                 // 1. open through closeHtmlTag
696                                 // 2. expression
697                                 // 3. openHtmlEnd through close
698                                 // This will allow recording the positions to reconstruct if HTML is to be treated as text.
700                                 startOpenTagPos = pos;
701                                 parsedOpenTagResult = sequence( [
702                                         openHtmlStartTag,
703                                         asciiAlphabetLiteral,
704                                         htmlAttributes,
705                                         optionalForwardSlash,
706                                         closeHtmlTag
707                                 ] );
709                                 if ( parsedOpenTagResult === null ) {
710                                         return null;
711                                 }
713                                 endOpenTagPos = pos;
714                                 startTagName = parsedOpenTagResult[1];
716                                 parsedHtmlContents = nOrMore( 0, expression )();
718                                 startCloseTagPos = pos;
719                                 parsedCloseTagResult = sequence( [
720                                         openHtmlEndTag,
721                                         asciiAlphabetLiteral,
722                                         closeHtmlTag
723                                 ] );
725                                 if ( parsedCloseTagResult === null ) {
726                                         // Closing tag failed.  Return the start tag and contents.
727                                         return [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ]
728                                                 .concat( parsedHtmlContents );
729                                 }
731                                 endCloseTagPos = pos;
732                                 endTagName = parsedCloseTagResult[1];
733                                 wrappedAttributes = parsedOpenTagResult[2];
734                                 attributes = wrappedAttributes.slice( 1 );
735                                 if ( isAllowedHtml( startTagName, endTagName, attributes ) ) {
736                                         result = [ 'HTMLELEMENT', startTagName, wrappedAttributes ]
737                                                 .concat( parsedHtmlContents );
738                                 } else {
739                                         // HTML is not allowed, so contents will remain how
740                                         // it was, while HTML markup at this level will be
741                                         // treated as text
742                                         // E.g. assuming script tags are not allowed:
743                                         //
744                                         // <script>[[Foo|bar]]</script>
745                                         //
746                                         // results in '&lt;script&gt;' and '&lt;/script&gt;'
747                                         // (not treated as an HTML tag), surrounding a fully
748                                         // parsed HTML link.
749                                         //
750                                         // Concatenate everything from the tag, flattening the contents.
751                                         result = [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ]
752                                                 .concat( parsedHtmlContents, input.slice( startCloseTagPos, endCloseTagPos ) );
753                                 }
755                                 return result;
756                         }
758                         templateName = transform(
759                                 // see $wgLegalTitleChars
760                                 // not allowing : due to the need to catch "PLURAL:$1"
761                                 makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+\-]+/ ),
762                                 function ( result ) { return result.toString(); }
763                         );
764                         function templateParam() {
765                                 var expr, result;
766                                 result = sequence( [
767                                         pipe,
768                                         nOrMore( 0, paramExpression )
769                                 ] );
770                                 if ( result === null ) {
771                                         return null;
772                                 }
773                                 expr = result[1];
774                                 // use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
775                                 return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[0];
776                         }
778                         function templateWithReplacement() {
779                                 var result = sequence( [
780                                         templateName,
781                                         colon,
782                                         replacement
783                                 ] );
784                                 return result === null ? null : [ result[0], result[2] ];
785                         }
786                         function templateWithOutReplacement() {
787                                 var result = sequence( [
788                                         templateName,
789                                         colon,
790                                         paramExpression
791                                 ] );
792                                 return result === null ? null : [ result[0], result[2] ];
793                         }
794                         function templateWithOutFirstParameter() {
795                                 var result = sequence( [
796                                         templateName,
797                                         colon
798                                 ] );
799                                 return result === null ? null : [ result[0], '' ];
800                         }
801                         colon = makeStringParser( ':' );
802                         templateContents = choice( [
803                                 function () {
804                                         var res = sequence( [
805                                                 // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
806                                                 // or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}}
807                                                 choice( [ templateWithReplacement, templateWithOutReplacement, templateWithOutFirstParameter ] ),
808                                                 nOrMore( 0, templateParam )
809                                         ] );
810                                         return res === null ? null : res[0].concat( res[1] );
811                                 },
812                                 function () {
813                                         var res = sequence( [
814                                                 templateName,
815                                                 nOrMore( 0, templateParam )
816                                         ] );
817                                         if ( res === null ) {
818                                                 return null;
819                                         }
820                                         return [ res[0] ].concat( res[1] );
821                                 }
822                         ] );
823                         openTemplate = makeStringParser( '{{' );
824                         closeTemplate = makeStringParser( '}}' );
825                         nonWhitespaceExpression = choice( [
826                                 template,
827                                 wikilink,
828                                 extLinkParam,
829                                 extlink,
830                                 replacement,
831                                 literalWithoutSpace
832                         ] );
833                         paramExpression = choice( [
834                                 template,
835                                 wikilink,
836                                 extLinkParam,
837                                 extlink,
838                                 replacement,
839                                 literalWithoutBar
840                         ] );
842                         expression = choice( [
843                                 template,
844                                 wikilink,
845                                 extLinkParam,
846                                 extlink,
847                                 replacement,
848                                 html,
849                                 literal
850                         ] );
852                         // Used when only {{-transformation is wanted, for 'text'
853                         // or 'escaped' formats
854                         curlyBraceTransformExpression = choice( [
855                                 template,
856                                 replacement,
857                                 curlyBraceTransformExpressionLiteral
858                         ] );
860                         /**
861                          * Starts the parse
862                          *
863                          * @param {Function} rootExpression root parse function
864                          */
865                         function start( rootExpression ) {
866                                 var result = nOrMore( 0, rootExpression )();
867                                 if ( result === null ) {
868                                         return null;
869                                 }
870                                 return [ 'CONCAT' ].concat( result );
871                         }
872                         // everything above this point is supposed to be stateless/static, but
873                         // I am deferring the work of turning it into prototypes & objects. It's quite fast enough
874                         // finally let's do some actual work...
876                         // If you add another possible rootExpression, you must update the astCache key scheme.
877                         result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression );
879                         /*
880                          * For success, the p must have gotten to the end of the input
881                          * and returned a non-null.
882                          * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
883                          */
884                         if ( result === null || pos !== input.length ) {
885                                 throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input );
886                         }
887                         return result;
888                 }
890         };
892         /**
893          * htmlEmitter - object which primarily exists to emit HTML from parser ASTs
894          */
895         mw.jqueryMsg.htmlEmitter = function ( language, magic ) {
896                 this.language = language;
897                 var jmsg = this;
898                 $.each( magic, function ( key, val ) {
899                         jmsg[ key.toLowerCase() ] = function () {
900                                 return val;
901                         };
902                 } );
904                 /**
905                  * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
906                  * Walk entire node structure, applying replacements and template functions when appropriate
907                  * @param {Mixed} node Abstract syntax tree (top node or subnode)
908                  * @param {Array} replacements for $1, $2, ... $n
909                  * @return {Mixed} single-string node or array of nodes suitable for jQuery appending
910                  */
911                 this.emit = function ( node, replacements ) {
912                         var ret, subnodes, operation,
913                                 jmsg = this;
914                         switch ( typeof node ) {
915                                 case 'string':
916                                 case 'number':
917                                         ret = node;
918                                         break;
919                                 // typeof returns object for arrays
920                                 case 'object':
921                                         // node is an array of nodes
922                                         subnodes = $.map( node.slice( 1 ), function ( n ) {
923                                                 return jmsg.emit( n, replacements );
924                                         } );
925                                         operation = node[0].toLowerCase();
926                                         if ( typeof jmsg[operation] === 'function' ) {
927                                                 ret = jmsg[ operation ]( subnodes, replacements );
928                                         } else {
929                                                 throw new Error( 'Unknown operation "' + operation + '"' );
930                                         }
931                                         break;
932                                 case 'undefined':
933                                         // Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined
934                                         // Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information?
935                                         // The logical thing is probably to return the empty string here when we encounter undefined.
936                                         ret = '';
937                                         break;
938                                 default:
939                                         throw new Error( 'Unexpected type in AST: ' + typeof node );
940                         }
941                         return ret;
942                 };
943         };
945         // For everything in input that follows double-open-curly braces, there should be an equivalent parser
946         // function. For instance {{PLURAL ... }} will be processed by 'plural'.
947         // If you have 'magic words' then configure the parser to have them upon creation.
948         //
949         // An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
950         // Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
951         mw.jqueryMsg.htmlEmitter.prototype = {
952                 /**
953                  * Parsing has been applied depth-first we can assume that all nodes here are single nodes
954                  * Must return a single node to parents -- a jQuery with synthetic span
955                  * However, unwrap any other synthetic spans in our children and pass them upwards
956                  * @param {Mixed[]} nodes Some single nodes, some arrays of nodes
957                  * @return {jQuery}
958                  */
959                 concat: function ( nodes ) {
960                         var $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
961                         $.each( nodes, function ( i, node ) {
962                                 if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) {
963                                         $.each( node.contents(), function ( j, childNode ) {
964                                                 appendWithoutParsing( $span, childNode );
965                                         } );
966                                 } else {
967                                         // Let jQuery append nodes, arrays of nodes and jQuery objects
968                                         // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
969                                         appendWithoutParsing( $span, node );
970                                 }
971                         } );
972                         return $span;
973                 },
975                 /**
976                  * Return escaped replacement of correct index, or string if unavailable.
977                  * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
978                  * if the specified parameter is not found return the same string
979                  * (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
980                  *
981                  * TODO: Throw error if nodes.length > 1 ?
982                  *
983                  * @param {Array} nodes List of one element, integer, n >= 0
984                  * @param {Array} replacements List of at least n strings
985                  * @return {String} replacement
986                  */
987                 replace: function ( nodes, replacements ) {
988                         var index = parseInt( nodes[0], 10 );
990                         if ( index < replacements.length ) {
991                                 return replacements[index];
992                         } else {
993                                 // index not found, fallback to displaying variable
994                                 return '$' + ( index + 1 );
995                         }
996                 },
998                 /**
999                  * Transform wiki-link
1000                  *
1001                  * TODO:
1002                  * It only handles basic cases, either no pipe, or a pipe with an explicit
1003                  * anchor.
1004                  *
1005                  * It does not attempt to handle features like the pipe trick.
1006                  * However, the pipe trick should usually not be present in wikitext retrieved
1007                  * from the server, since the replacement is done at save time.
1008                  * It may, though, if the wikitext appears in extension-controlled content.
1009                  *
1010                  * @param nodes
1011                  */
1012                 wikilink: function ( nodes ) {
1013                         var page, anchor, url;
1015                         page = nodes[0];
1016                         url = mw.util.getUrl( page );
1018                         // [[Some Page]] or [[Namespace:Some Page]]
1019                         if ( nodes.length === 1 ) {
1020                                 anchor = page;
1021                         }
1023                         /*
1024                          * [[Some Page|anchor text]] or
1025                          * [[Namespace:Some Page|anchor]
1026                          */
1027                         else {
1028                                 anchor = nodes[1];
1029                         }
1031                         return $( '<a>' ).attr( {
1032                                 title: page,
1033                                 href: url
1034                         } ).text( anchor );
1035                 },
1037                 /**
1038                  * Converts array of HTML element key value pairs to object
1039                  *
1040                  * @param {Array} nodes Array of consecutive key value pairs, with index 2 * n being a
1041                  *  name and 2 * n + 1 the associated value
1042                  * @return {Object} Object mapping attribute name to attribute value
1043                  */
1044                 htmlattributes: function ( nodes ) {
1045                         var i, len, mapping = {};
1046                         for ( i = 0, len = nodes.length; i < len; i += 2 ) {
1047                                 mapping[nodes[i]] = decodePrimaryHtmlEntities( nodes[i + 1] );
1048                         }
1049                         return mapping;
1050                 },
1052                 /**
1053                  * Handles an (already-validated) HTML element.
1054                  *
1055                  * @param {Array} nodes Nodes to process when creating element
1056                  * @return {jQuery|Array} jQuery node for valid HTML or array for disallowed element
1057                  */
1058                 htmlelement: function ( nodes ) {
1059                         var tagName, attributes, contents, $element;
1061                         tagName = nodes.shift();
1062                         attributes = nodes.shift();
1063                         contents = nodes;
1064                         $element = $( document.createElement( tagName ) ).attr( attributes );
1065                         return appendWithoutParsing( $element, contents );
1066                 },
1068                 /**
1069                  * Transform parsed structure into external link
1070                  * If the href is a jQuery object, treat it as "enclosing" the link text.
1071                  *
1072                  * - ... function, treat it as the click handler.
1073                  * - ... string, treat it as a URI.
1074                  *
1075                  * TODO: throw an error if nodes.length > 2 ?
1076                  *
1077                  * @param {Array} nodes List of two elements, {jQuery|Function|String} and {String}
1078                  * @return {jQuery}
1079                  */
1080                 extlink: function ( nodes ) {
1081                         var $el,
1082                                 arg = nodes[0],
1083                                 contents = nodes[1];
1084                         if ( arg instanceof jQuery ) {
1085                                 $el = arg;
1086                         } else {
1087                                 $el = $( '<a>' );
1088                                 if ( typeof arg === 'function' ) {
1089                                         $el.attr( 'href', '#' )
1090                                         .click( function ( e ) {
1091                                                 e.preventDefault();
1092                                         } )
1093                                         .click( arg );
1094                                 } else {
1095                                         $el.attr( 'href', arg.toString() );
1096                                 }
1097                         }
1098                         return appendWithoutParsing( $el, contents );
1099                 },
1101                 /**
1102                  * This is basically use a combination of replace + external link (link with parameter
1103                  * as url), but we don't want to run the regular replace here-on: inserting a
1104                  * url as href-attribute of a link will automatically escape it already, so
1105                  * we don't want replace to (manually) escape it as well.
1106                  *
1107                  * TODO: throw error if nodes.length > 1 ?
1108                  *
1109                  * @param {Array} nodes List of one element, integer, n >= 0
1110                  * @param {Array} replacements List of at least n strings
1111                  * @return {string} replacement
1112                  */
1113                 extlinkparam: function ( nodes, replacements ) {
1114                         var replacement,
1115                                 index = parseInt( nodes[0], 10 );
1116                         if ( index < replacements.length ) {
1117                                 replacement = replacements[index];
1118                         } else {
1119                                 replacement = '$' + ( index + 1 );
1120                         }
1121                         return this.extlink( [ replacement, nodes[1] ] );
1122                 },
1124                 /**
1125                  * Transform parsed structure into pluralization
1126                  * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
1127                  * So convert it back with the current language's convertNumber.
1128                  * @param {Array} nodes List of nodes, [ {string|number}, {string}, {string} ... ]
1129                  * @return {string} selected pluralized form according to current language
1130                  */
1131                 plural: function ( nodes ) {
1132                         var forms, firstChild, firstChildText,
1133                                 explicitPluralForms = {}, explicitPluralFormNumber, formIndex, form, count;
1135                         count = parseFloat( this.language.convertNumber( nodes[0], true ) );
1136                         forms = nodes.slice( 1 );
1137                         for ( formIndex = 0; formIndex < forms.length; formIndex++ ) {
1138                                 form = forms[formIndex];
1140                                 if ( form.jquery && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
1141                                         // This is a nested node, may be an explicit plural form like 5=[$2 linktext]
1142                                         firstChild = form.contents().get( 0 );
1143                                         if ( firstChild && firstChild.nodeType === Node.TEXT_NODE ) {
1144                                                 firstChildText = firstChild.textContent;
1145                                                 if ( /^\d+=/.test( firstChildText ) ) {
1146                                                         explicitPluralFormNumber = parseInt( firstChildText.split( /=/ )[0], 10 );
1147                                                         // Use the digit part as key and rest of first text node and
1148                                                         // rest of child nodes as value.
1149                                                         firstChild.textContent = firstChildText.slice( firstChildText.indexOf( '=' ) + 1 );
1150                                                         explicitPluralForms[explicitPluralFormNumber] = form;
1151                                                         forms[formIndex] = undefined;
1152                                                 }
1153                                         }
1154                                 } else if ( /^\d+=/.test( form ) ) {
1155                                         // Simple explicit plural forms like 12=a dozen
1156                                         explicitPluralFormNumber = parseInt( form.split( /=/ )[0], 10 );
1157                                         explicitPluralForms[explicitPluralFormNumber] = form.slice( form.indexOf( '=' ) + 1 );
1158                                         forms[formIndex] = undefined;
1159                                 }
1160                         }
1162                         // Remove explicit plural forms from the forms. They were set undefined in the above loop.
1163                         forms = $.map( forms, function ( form ) {
1164                                 return form;
1165                         } );
1167                         return this.language.convertPlural( count, forms, explicitPluralForms );
1168                 },
1170                 /**
1171                  * Transform parsed structure according to gender.
1172                  *
1173                  * Usage: {{gender:[ mw.user object | '' | 'male' | 'female' | 'unknown' ] | masculine form | feminine form | neutral form}}.
1174                  *
1175                  * The first node must be one of:
1176                  * - the mw.user object (or a compatible one)
1177                  * - an empty string - indicating the current user, same effect as passing the mw.user object
1178                  * - a gender string ('male', 'female' or 'unknown')
1179                  *
1180                  * @param {Array} nodes List of nodes, [ {string|mw.user}, {string}, {string}, {string} ]
1181                  * @return {string} Selected gender form according to current language
1182                  */
1183                 gender: function ( nodes ) {
1184                         var gender,
1185                                 maybeUser = nodes[0],
1186                                 forms = nodes.slice( 1 );
1188                         if ( maybeUser === '' ) {
1189                                 maybeUser = mw.user;
1190                         }
1192                         // If we are passed a mw.user-like object, check their gender.
1193                         // Otherwise, assume the gender string itself was passed .
1194                         if ( maybeUser && maybeUser.options instanceof mw.Map ) {
1195                                 gender = maybeUser.options.get( 'gender' );
1196                         } else {
1197                                 gender = maybeUser;
1198                         }
1200                         return this.language.gender( gender, forms );
1201                 },
1203                 /**
1204                  * Transform parsed structure into grammar conversion.
1205                  * Invoked by putting `{{grammar:form|word}}` in a message
1206                  * @param {Array} nodes List of nodes [{Grammar case eg: genitive}, {string word}]
1207                  * @return {string} selected grammatical form according to current language
1208                  */
1209                 grammar: function ( nodes ) {
1210                         var form = nodes[0],
1211                                 word = nodes[1];
1212                         return word && form && this.language.convertGrammar( word, form );
1213                 },
1215                 /**
1216                  * Tranform parsed structure into a int: (interface language) message include
1217                  * Invoked by putting `{{int:othermessage}}` into a message
1218                  * @param {Array} nodes List of nodes
1219                  * @return {string} Other message
1220                  */
1221                 'int': function ( nodes ) {
1222                         return mw.jqueryMsg.getMessageFunction()( nodes[0].toLowerCase() );
1223                 },
1225                 /**
1226                  * Takes an unformatted number (arab, no group separators and . as decimal separator)
1227                  * and outputs it in the localized digit script and formatted with decimal
1228                  * separator, according to the current language.
1229                  * @param {Array} nodes List of nodes
1230                  * @return {number|string} Formatted number
1231                  */
1232                 formatnum: function ( nodes ) {
1233                         var isInteger = ( nodes[1] && nodes[1] === 'R' ) ? true : false,
1234                                 number = nodes[0];
1236                         return this.language.convertNumber( number, isInteger );
1237                 }
1238         };
1240         // Deprecated! don't rely on gM existing.
1241         // The window.gM ought not to be required - or if required, not required here.
1242         // But moving it to extensions breaks it (?!)
1243         // Need to fix plugin so it could do attributes as well, then will be okay to remove this.
1244         // @deprecated since 1.23
1245         mw.log.deprecate( window, 'gM', mw.jqueryMsg.getMessageFunction(), 'Use mw.message( ... ).parse() instead.' );
1247         /**
1248          * @method
1249          * @member jQuery
1250          * @see mw.jqueryMsg#getPlugin
1251          */
1252         $.fn.msg = mw.jqueryMsg.getPlugin();
1254         // Replace the default message parser with jqueryMsg
1255         oldParser = mw.Message.prototype.parser;
1256         mw.Message.prototype.parser = function () {
1257                 var messageFunction;
1259                 // TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe?
1260                 // Caching is somewhat problematic, because we do need different message functions for different maps, so
1261                 // we'd have to cache the parser as a member of this.map, which sounds a bit ugly.
1262                 // Do not use mw.jqueryMsg unless required
1263                 if ( this.format === 'plain' || !/\{\{|[\[<>]/.test( this.map.get( this.key ) ) ) {
1264                         // Fall back to mw.msg's simple parser
1265                         return oldParser.apply( this );
1266                 }
1268                 messageFunction = mw.jqueryMsg.getMessageFunction( {
1269                         'messages': this.map,
1270                         // For format 'escaped', escaping part is handled by mediawiki.js
1271                         'format': this.format
1272                 } );
1273                 return messageFunction( this.key, this.parameters );
1274         };
1276 }( mediaWiki, jQuery ) );