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