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