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