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