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