Merge "Added release notes for 'ContentHandler::runLegacyHooks' removal"
[mediawiki.git] / resources / lib / jquery.i18n / src / jquery.i18n.parser.js
blob3dea28425d061cde863f9ac431fb16f96f664337
1 /**
2  * jQuery Internationalization library
3  *
4  * Copyright (C) 2011-2013 Santhosh Thottingal, Neil Kandalgaonkar
5  *
6  * jquery.i18n is dual licensed GPLv2 or later and MIT. You don't have to do
7  * anything special to choose one license or the other and you don't have to
8  * notify anyone which license you are using. You are free to use
9  * UniversalLanguageSelector in commercial projects as long as the copyright
10  * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details.
11  *
12  * @licence GNU General Public Licence 2.0 or later
13  * @licence MIT License
14  */
16 ( function ( $ ) {
17         'use strict';
19         var MessageParser = function ( options ) {
20                 this.options = $.extend( {}, $.i18n.parser.defaults, options );
21                 this.language = $.i18n.languages[String.locale] || $.i18n.languages['default'];
22                 this.emitter = $.i18n.parser.emitter;
23         };
25         MessageParser.prototype = {
27                 constructor: MessageParser,
29                 simpleParse: function ( message, parameters ) {
30                         return message.replace( /\$(\d+)/g, function ( str, match ) {
31                                 var index = parseInt( match, 10 ) - 1;
33                                 return parameters[index] !== undefined ? parameters[index] : '$' + match;
34                         } );
35                 },
37                 parse: function ( message, replacements ) {
38                         if ( message.indexOf( '{{' ) < 0 ) {
39                                 return this.simpleParse( message, replacements );
40                         }
42                         this.emitter.language = $.i18n.languages[$.i18n().locale] ||
43                                 $.i18n.languages['default'];
45                         return this.emitter.emit( this.ast( message ), replacements );
46                 },
48                 ast: function ( message ) {
49                         var pipe, colon, backslash, anyCharacter, dollar, digits, regularLiteral,
50                                 regularLiteralWithoutBar, regularLiteralWithoutSpace, escapedOrLiteralWithoutBar,
51                                 escapedOrRegularLiteral, templateContents, templateName, openTemplate,
52                                 closeTemplate, expression, paramExpression, result,
53                                 pos = 0;
55                         // Try parsers until one works, if none work return null
56                         function choice( parserSyntax ) {
57                                 return function () {
58                                         var i, result;
60                                         for ( i = 0; i < parserSyntax.length; i++ ) {
61                                                 result = parserSyntax[i]();
63                                                 if ( result !== null ) {
64                                                         return result;
65                                                 }
66                                         }
68                                         return null;
69                                 };
70                         }
72                         // Try several parserSyntax-es in a row.
73                         // All must succeed; otherwise, return null.
74                         // This is the only eager one.
75                         function sequence( parserSyntax ) {
76                                 var i, res,
77                                         originalPos = pos,
78                                         result = [];
80                                 for ( i = 0; i < parserSyntax.length; i++ ) {
81                                         res = parserSyntax[i]();
83                                         if ( res === null ) {
84                                                 pos = originalPos;
86                                                 return null;
87                                         }
89                                         result.push( res );
90                                 }
92                                 return result;
93                         }
95                         // Run the same parser over and over until it fails.
96                         // Must succeed a minimum of n times; otherwise, return null.
97                         function nOrMore( n, p ) {
98                                 return function () {
99                                         var originalPos = pos,
100                                                 result = [],
101                                                 parsed = p();
103                                         while ( parsed !== null ) {
104                                                 result.push( parsed );
105                                                 parsed = p();
106                                         }
108                                         if ( result.length < n ) {
109                                                 pos = originalPos;
111                                                 return null;
112                                         }
114                                         return result;
115                                 };
116                         }
118                         // Helpers -- just make parserSyntax out of simpler JS builtin types
120                         function makeStringParser( s ) {
121                                 var len = s.length;
123                                 return function () {
124                                         var result = null;
126                                         if ( message.substr( pos, len ) === s ) {
127                                                 result = s;
128                                                 pos += len;
129                                         }
131                                         return result;
132                                 };
133                         }
135                         function makeRegexParser( regex ) {
136                                 return function () {
137                                         var matches = message.substr( pos ).match( regex );
139                                         if ( matches === null ) {
140                                                 return null;
141                                         }
143                                         pos += matches[0].length;
145                                         return matches[0];
146                                 };
147                         }
149                         pipe = makeStringParser( '|' );
150                         colon = makeStringParser( ':' );
151                         backslash = makeStringParser( '\\' );
152                         anyCharacter = makeRegexParser( /^./ );
153                         dollar = makeStringParser( '$' );
154                         digits = makeRegexParser( /^\d+/ );
155                         regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ );
156                         regularLiteralWithoutBar = makeRegexParser( /^[^{}\[\]$\\|]/ );
157                         regularLiteralWithoutSpace = makeRegexParser( /^[^{}\[\]$\s]/ );
159                         // There is a general pattern:
160                         // parse a thing;
161                         // if it worked, apply transform,
162                         // otherwise return null.
163                         // But using this as a combinator seems to cause problems
164                         // when combined with nOrMore().
165                         // May be some scoping issue.
166                         function transform( p, fn ) {
167                                 return function () {
168                                         var result = p();
170                                         return result === null ? null : fn( result );
171                                 };
172                         }
174                         // Used to define "literals" within template parameters. The pipe
175                         // character is the parameter delimeter, so by default
176                         // it is not a literal in the parameter
177                         function literalWithoutBar() {
178                                 var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
180                                 return result === null ? null : result.join( '' );
181                         }
183                         function literal() {
184                                 var result = nOrMore( 1, escapedOrRegularLiteral )();
186                                 return result === null ? null : result.join( '' );
187                         }
189                         function escapedLiteral() {
190                                 var result = sequence( [ backslash, anyCharacter ] );
192                                 return result === null ? null : result[1];
193                         }
195                         choice( [ escapedLiteral, regularLiteralWithoutSpace ] );
196                         escapedOrLiteralWithoutBar = choice( [ escapedLiteral, regularLiteralWithoutBar ] );
197                         escapedOrRegularLiteral = choice( [ escapedLiteral, regularLiteral ] );
199                         function replacement() {
200                                 var result = sequence( [ dollar, digits ] );
202                                 if ( result === null ) {
203                                         return null;
204                                 }
206                                 return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ];
207                         }
209                         templateName = transform(
210                                 // see $wgLegalTitleChars
211                                 // not allowing : due to the need to catch "PLURAL:$1"
212                                 makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+\-]+/ ),
214                                 function ( result ) {
215                                         return result.toString();
216                                 }
217                         );
219                         function templateParam() {
220                                 var expr,
221                                         result = sequence( [ pipe, nOrMore( 0, paramExpression ) ] );
223                                 if ( result === null ) {
224                                         return null;
225                                 }
227                                 expr = result[1];
229                                 // use a "CONCAT" operator if there are multiple nodes,
230                                 // otherwise return the first node, raw.
231                                 return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[0];
232                         }
234                         function templateWithReplacement() {
235                                 var result = sequence( [ templateName, colon, replacement ] );
237                                 return result === null ? null : [ result[0], result[2] ];
238                         }
240                         function templateWithOutReplacement() {
241                                 var result = sequence( [ templateName, colon, paramExpression ] );
243                                 return result === null ? null : [ result[0], result[2] ];
244                         }
246                         templateContents = choice( [
247                                 function () {
248                                         var res = sequence( [
249                                                 // templates can have placeholders for dynamic
250                                                 // replacement eg: {{PLURAL:$1|one car|$1 cars}}
251                                                 // or no placeholders eg:
252                                                 // {{GRAMMAR:genitive|{{SITENAME}}}
253                                                 choice( [ templateWithReplacement, templateWithOutReplacement ] ),
254                                                 nOrMore( 0, templateParam )
255                                         ] );
257                                         return res === null ? null : res[0].concat( res[1] );
258                                 },
259                                 function () {
260                                         var res = sequence( [ templateName, nOrMore( 0, templateParam ) ] );
262                                         if ( res === null ) {
263                                                 return null;
264                                         }
266                                         return [ res[0] ].concat( res[1] );
267                                 }
268                         ] );
270                         openTemplate = makeStringParser( '{{' );
271                         closeTemplate = makeStringParser( '}}' );
273                         function template() {
274                                 var result = sequence( [ openTemplate, templateContents, closeTemplate ] );
276                                 return result === null ? null : result[1];
277                         }
279                         expression = choice( [ template, replacement, literal ] );
280                         paramExpression = choice( [ template, replacement, literalWithoutBar ] );
282                         function start() {
283                                 var result = nOrMore( 0, expression )();
285                                 if ( result === null ) {
286                                         return null;
287                                 }
289                                 return [ 'CONCAT' ].concat( result );
290                         }
292                         result = start();
294                         /*
295                          * For success, the pos must have gotten to the end of the input
296                          * and returned a non-null.
297                          * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
298                          */
299                         if ( result === null || pos !== message.length ) {
300                                 throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + message );
301                         }
303                         return result;
304                 }
306         };
308         $.extend( $.i18n.parser, new MessageParser() );
309 }( jQuery ) );