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