Merge "Make update.php file executable"
[mediawiki.git] / resources / src / mediawiki / mediawiki.jqueryMsg.js
blob37317718538e0e64cebc6ed43899d12212662c80
1 /*!
2 * Experimental advanced wikitext parser-emitter.
3 * See: https://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs
5 * @author neilk@wikimedia.org
6 * @author mflaschen@wikimedia.org
7 */
8 ( function ( mw, $ ) {
9 /**
10 * @class mw.jqueryMsg
11 * @singleton
14 var oldParser,
15 slice = Array.prototype.slice,
16 parserDefaults = {
17 magic: {
18 'SITENAME': mw.config.get( 'wgSiteName' )
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'
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',
37 // WAI-ARIA
38 'role'
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,
48 // Same meaning as in mediawiki.js.
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).
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'
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.
65 * Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is.
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
72 function appendWithoutParsing( $parent, children ) {
73 var i, len;
75 if ( !$.isArray( children ) ) {
76 children = [children];
79 for ( i = 0, len = children.length; i < len; i++ ) {
80 if ( typeof children[i] !== 'object' ) {
81 children[i] = document.createTextNode( children[i] );
85 return $parent.append( children );
88 /**
89 * Decodes the main HTML entities, those encoded by mw.html.escape.
91 * @private
92 * @param {string} encoded Encoded string
93 * @return {string} String with those entities decoded
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, '&' );
105 * Given parser options, return a function that parses a key and replacements, returning jQuery object
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
116 function getFailableParserFn( options ) {
117 var parser = new mw.jqueryMsg.parser( options );
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 );
132 mw.jqueryMsg = {};
135 * Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements).
136 * e.g.
138 * window.gM = mediaWiki.parser.getMessageFunction( options );
139 * $( 'p#headline' ).html( gM( 'hello-user', username ) );
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().
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] )
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.
155 mw.jqueryMsg.getMessageFunction = function ( options ) {
156 var failableParserFn = getFailableParserFn( options ),
157 format;
159 if ( options && options.format !== undefined ) {
160 format = options.format;
161 } else {
162 format = parserDefaults.format;
165 return function () {
166 var failableResult = failableParserFn( arguments );
167 if ( format === 'text' || format === 'escaped' ) {
168 return failableResult.text();
169 } else {
170 return failableResult.html();
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.
180 * $.fn.msg = mediaWiki.parser.getJqueryPlugin( options );
181 * var userlink = $( '<a>' ).click( function () { alert( "hello!!" ) } );
182 * $( 'p#headline' ).msg( 'hello-user', userlink );
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] )
189 * We append to 'this', which in a jQuery plugin context will be the selected elements.
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
197 mw.jqueryMsg.getPlugin = function ( options ) {
198 var failableParserFn = getFailableParserFn( options );
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;
212 * The parser itself.
213 * Describes an object, whose primary duty is to .parse() message keys.
215 * @class
216 * @private
217 * @param {Object} options
219 mw.jqueryMsg.parser = function ( options ) {
220 this.settings = $.extend( {}, parserDefaults, options );
221 this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' );
223 this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic );
226 mw.jqueryMsg.parser.prototype = {
228 * Cache mapping MediaWiki message keys and the value onlyCurlyBraceTransform, to the AST of the message.
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).
233 * The two parts of the key are separated by colon. For example:
235 * "message-key:true": ast
237 * if they key is "message-key" and onlyCurlyBraceTransform is true.
239 * This cache is shared by all instances of mw.jqueryMsg.parser.
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.
245 * @static
246 * @property {Object}
248 astCache: {},
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}
258 parse: function ( key, replacements ) {
259 return this.emitter.emit( this.getAst( key ), replacements );
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
268 getAst: function ( key ) {
269 var cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' ), wikiText;
271 if ( this.astCache[ cacheKey ] === undefined ) {
272 wikiText = this.settings.messages.get( key );
273 if ( typeof wikiText !== 'string' ) {
274 wikiText = '\\[' + key + '\\]';
276 this.astCache[ cacheKey ] = this.wikiTextToAst( wikiText );
278 return this.astCache[ cacheKey ];
282 * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
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.
287 * @param {string} input Message string wikitext
288 * @throws Error
289 * @return {Mixed} abstract syntax tree
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;
302 // Indicates current position in input as we parse through it.
303 // Shared among all parsing functions below.
304 pos = 0;
306 // =========================================================
307 // parsing combinators - could be a library on its own
308 // =========================================================
311 * Try parsers until one works, if none work return null
312 * @private
313 * @param {Function[]} ps
314 * @return {string|null}
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;
325 return null;
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}
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;
346 result.push( res );
348 return result;
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}
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();
368 if ( result.length < n ) {
369 pos = originalPos;
370 return null;
372 return result;
377 * There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null.
379 * TODO: But using this as a combinator seems to cause problems when combined with #nOrMore().
380 * May be some scoping issue
382 * @private
383 * @param {Function} p
384 * @param {Function} fn
385 * @return {string|null}
387 function transform( p, fn ) {
388 return function () {
389 var result = p();
390 return result === null ? null : fn( result );
395 * Just make parsers out of simpler JS builtin types
396 * @private
397 * @param {string} s
398 * @return {Function}
399 * @return {string} return.return
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;
409 return result;
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.
418 * @private
419 * @param {RegExp} regex anchored regex
420 * @return {Function} function to parse input based on the regex
422 function makeRegexParser( regex ) {
423 return function () {
424 var matches = input.substr( pos ).match( regex );
425 if ( matches === null ) {
426 return null;
428 pos += matches[0].length;
429 return matches[0];
433 // ===================================================================
434 // General patterns above this line -- wikitext specific parsers below
435 // ===================================================================
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
448 regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ );
449 regularLiteralWithoutBar = makeRegexParser( /^[^{}\[\]$\\|]/ );
450 regularLiteralWithoutSpace = makeRegexParser( /^[^{}\[\]$\s]/ );
451 regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ );
453 backslash = makeStringParser( '\\' );
454 doubleQuote = makeStringParser( '"' );
455 singleQuote = makeStringParser( '\'' );
456 anyCharacter = makeRegexParser( /^./ );
458 openHtmlStartTag = makeStringParser( '<' );
459 optionalForwardSlash = makeRegexParser( /^\/?/ );
460 openHtmlEndTag = makeStringParser( '</' );
461 htmlAttributeEquals = makeRegexParser( /^\s*=\s*/ );
462 closeHtmlTag = makeRegexParser( /^\s*>/ );
464 function escapedLiteral() {
465 var result = sequence( [
466 backslash,
467 anyCharacter
468 ] );
469 return result === null ? null : result[1];
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( '' );
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( '' );
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( '' );
502 function literal() {
503 var result = nOrMore( 1, escapedOrRegularLiteral )();
504 return result === null ? null : result.join( '' );
507 function curlyBraceTransformExpressionLiteral() {
508 var result = nOrMore( 1, regularLiteralWithSquareBrackets )();
509 return result === null ? null : result.join( '' );
512 asciiAlphabetLiteral = makeRegexParser( /[A-Za-z]+/ );
513 htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ );
514 htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ );
516 whitespace = makeRegexParser( /^\s+/ );
517 dollar = makeStringParser( '$' );
518 digits = makeRegexParser( /^\d+/ );
520 function replacement() {
521 var result = sequence( [
522 dollar,
523 digits
524 ] );
525 if ( result === null ) {
526 return null;
528 return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ];
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] ) );
553 return result;
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;
568 return [ 'EXTLINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ];
570 openWikilink = makeStringParser( '[[' );
571 closeWikilink = makeStringParser( ']]' );
572 pipe = makeStringParser( '|' );
574 function template() {
575 var result = sequence( [
576 openTemplate,
577 templateContents,
578 closeTemplate
579 ] );
580 return result === null ? null : result[1];
583 wikilinkPage = choice( [
584 unescapedLiteralWithoutBar,
585 template
586 ] );
588 function pipedWikilink() {
589 var result = sequence( [
590 wikilinkPage,
591 pipe,
592 expression
593 ] );
594 return result === null ? null : [ result[0], result[2] ];
597 wikilinkContents = choice( [
598 pipedWikilink,
599 wikilinkPage // unpiped link
600 ] );
602 function wikilink() {
603 var result, parsedResult, parsedLinkContents;
604 result = null;
606 parsedResult = sequence( [
607 openWikilink,
608 wikilinkContents,
609 closeWikilink
610 ] );
611 if ( parsedResult !== null ) {
612 parsedLinkContents = parsedResult[1];
613 result = [ 'WIKILINK' ].concat( parsedLinkContents );
615 return result;
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];
628 function singleQuotedHtmlAttributeValue() {
629 var parsedResult = sequence( [
630 singleQuote,
631 htmlSingleQuoteAttributeValue,
632 singleQuote
633 ] );
634 return parsedResult === null ? null : parsedResult[1];
637 function htmlAttribute() {
638 var parsedResult = sequence( [
639 whitespace,
640 asciiAlphabetLiteral,
641 htmlAttributeEquals,
642 choice( [
643 doubleQuotedHtmlAttributeValue,
644 singleQuotedHtmlAttributeValue
646 ] );
647 return parsedResult === null ? null : [parsedResult[1], parsedResult[3]];
651 * Checks if HTML is allowed
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
659 function isAllowedHtml( startTagName, endTagName, attributes ) {
660 var i, len, attributeName;
662 startTagName = startTagName.toLowerCase();
663 endTagName = endTagName.toLowerCase();
664 if ( startTagName !== endTagName || $.inArray( startTagName, settings.allowedHtmlElements ) === -1 ) {
665 return false;
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;
676 return true;
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 );
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;
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.
699 startOpenTagPos = pos;
700 parsedOpenTagResult = sequence( [
701 openHtmlStartTag,
702 asciiAlphabetLiteral,
703 htmlAttributes,
704 optionalForwardSlash,
705 closeHtmlTag
706 ] );
708 if ( parsedOpenTagResult === null ) {
709 return null;
712 endOpenTagPos = pos;
713 startTagName = parsedOpenTagResult[1];
715 parsedHtmlContents = nOrMore( 0, expression )();
717 startCloseTagPos = pos;
718 parsedCloseTagResult = sequence( [
719 openHtmlEndTag,
720 asciiAlphabetLiteral,
721 closeHtmlTag
722 ] );
724 if ( parsedCloseTagResult === null ) {
725 // Closing tag failed. Return the start tag and contents.
726 return [ 'CONCAT', input.substring( startOpenTagPos, endOpenTagPos ) ]
727 .concat( parsedHtmlContents );
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:
743 // <script>[[Foo|bar]]</script>
745 // results in '&lt;script&gt;' and '&lt;/script&gt;'
746 // (not treated as an HTML tag), surrounding a fully
747 // parsed HTML link.
749 // Concatenate everything from the tag, flattening the contents.
750 result = [ 'CONCAT', input.substring( startOpenTagPos, endOpenTagPos ) ]
751 .concat( parsedHtmlContents, input.substring( startCloseTagPos, endCloseTagPos ) );
754 return result;
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(); }
763 function templateParam() {
764 var expr, result;
765 result = sequence( [
766 pipe,
767 nOrMore( 0, paramExpression )
768 ] );
769 if ( result === null ) {
770 return null;
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];
777 function templateWithReplacement() {
778 var result = sequence( [
779 templateName,
780 colon,
781 replacement
782 ] );
783 return result === null ? null : [ result[0], result[2] ];
785 function templateWithOutReplacement() {
786 var result = sequence( [
787 templateName,
788 colon,
789 paramExpression
790 ] );
791 return result === null ? null : [ result[0], result[2] ];
793 function templateWithOutFirstParameter() {
794 var result = sequence( [
795 templateName,
796 colon
797 ] );
798 return result === null ? null : [ result[0], '' ];
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] );
811 function () {
812 var res = sequence( [
813 templateName,
814 nOrMore( 0, templateParam )
815 ] );
816 if ( res === null ) {
817 return null;
819 return [ res[0] ].concat( res[1] );
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 ] );
841 expression = choice( [
842 template,
843 wikilink,
844 extLinkParam,
845 extlink,
846 replacement,
847 html,
848 literal
849 ] );
851 // Used when only {{-transformation is wanted, for 'text'
852 // or 'escaped' formats
853 curlyBraceTransformExpression = choice( [
854 template,
855 replacement,
856 curlyBraceTransformExpressionLiteral
857 ] );
860 * Starts the parse
862 * @param {Function} rootExpression root parse function
864 function start( rootExpression ) {
865 var result = nOrMore( 0, rootExpression )();
866 if ( result === null ) {
867 return null;
869 return [ 'CONCAT' ].concat( result );
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...
875 // If you add another possible rootExpression, you must update the astCache key scheme.
876 result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression );
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.
883 if ( result === null || pos !== input.length ) {
884 throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input );
886 return result;
892 * htmlEmitter - object which primarily exists to emit HTML from parser ASTs
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;
901 } );
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
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 + '"' );
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 );
940 return ret;
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.
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 = {
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}
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 );
970 } );
971 return $span;
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" )
980 * TODO: Throw error if nodes.length > 1 ?
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
986 replace: function ( nodes, replacements ) {
987 var index = parseInt( nodes[0], 10 );
989 if ( index < replacements.length ) {
990 return replacements[index];
991 } else {
992 // index not found, fallback to displaying variable
993 return '$' + ( index + 1 );
998 * Transform wiki-link
1000 * TODO:
1001 * It only handles basic cases, either no pipe, or a pipe with an explicit
1002 * anchor.
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.
1009 * @param nodes
1011 wikilink: function ( nodes ) {
1012 var page, anchor, url;
1014 page = nodes[0];
1015 url = mw.util.getUrl( page );
1017 // [[Some Page]] or [[Namespace:Some Page]]
1018 if ( nodes.length === 1 ) {
1019 anchor = page;
1023 * [[Some Page|anchor text]] or
1024 * [[Namespace:Some Page|anchor]
1026 else {
1027 anchor = nodes[1];
1030 return $( '<a>' ).attr( {
1031 title: page,
1032 href: url
1033 } ).text( anchor );
1037 * Converts array of HTML element key value pairs to object
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
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] );
1048 return mapping;
1052 * Handles an (already-validated) HTML element.
1054 * @param {Array} nodes Nodes to process when creating element
1055 * @return {jQuery|Array} jQuery node for valid HTML or array for disallowed element
1057 htmlelement: function ( nodes ) {
1058 var tagName, attributes, contents, $element;
1060 tagName = nodes.shift();
1061 attributes = nodes.shift();
1062 contents = nodes;
1063 $element = $( document.createElement( tagName ) ).attr( attributes );
1064 return appendWithoutParsing( $element, contents );
1068 * Transform parsed structure into external link
1069 * If the href is a jQuery object, treat it as "enclosing" the link text.
1071 * - ... function, treat it as the click handler.
1072 * - ... string, treat it as a URI.
1074 * TODO: throw an error if nodes.length > 2 ?
1076 * @param {Array} nodes List of two elements, {jQuery|Function|String} and {String}
1077 * @return {jQuery}
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() );
1093 return appendWithoutParsing( $el, contents );
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.
1102 * TODO: throw error if nodes.length > 1 ?
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
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 );
1116 return this.extlink( [ replacement, nodes[1] ] );
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
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 ) : '';
1134 * Transform parsed structure according to gender.
1136 * Usage: {{gender:[ mw.user object | '' | 'male' | 'female' | 'unknown' ] | masculine form | feminine form | neutral form}}.
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')
1143 * @param {Array} nodes List of nodes, [ {string|mw.user}, {string}, {string}, {string} ]
1144 * @return {string} Selected gender form according to current language
1146 gender: function ( nodes ) {
1147 var gender,
1148 maybeUser = nodes[0],
1149 forms = nodes.slice( 1 );
1151 if ( maybeUser === '' ) {
1152 maybeUser = mw.user;
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;
1163 return this.language.gender( gender, forms );
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
1172 grammar: function ( nodes ) {
1173 var form = nodes[0],
1174 word = nodes[1];
1175 return word && form && this.language.convertGrammar( word, form );
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
1184 'int': function ( nodes ) {
1185 return mw.jqueryMsg.getMessageFunction()( nodes[0].toLowerCase() );
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
1195 formatnum: function ( nodes ) {
1196 var isInteger = ( nodes[1] && nodes[1] === 'R' ) ? true : false,
1197 number = nodes[0];
1199 return this.language.convertNumber( number, isInteger );
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.' );
1211 * @method
1212 * @member jQuery
1213 * @see mw.jqueryMsg#getPlugin
1215 $.fn.msg = mw.jqueryMsg.getPlugin();
1217 // Replace the default message parser with jqueryMsg
1218 oldParser = mw.Message.prototype.parser;
1219 mw.Message.prototype.parser = function () {
1220 var messageFunction;
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 );
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 );
1239 }( mediaWiki, jQuery ) );