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