Merge "Fix escaping for MSSQL LIKE queries."
[lhc/web/wiklou.git] / resources / src / mediawiki.libs / CLDRPluralRuleParser.js
1 /* This is CLDRPluralRuleParser v1.1.3, ported to MediaWiki ResourceLoader */
2
3 /**
4 * CLDRPluralRuleParser.js
5 * A parser engine for CLDR plural rules.
6 *
7 * Copyright 2012-2014 Santhosh Thottingal and other contributors
8 * Released under the MIT license
9 * http://opensource.org/licenses/MIT
10 *
11 * @source https://github.com/santhoshtr/CLDRPluralRuleParser
12 * @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
13 * @author Timo Tijhof
14 * @author Amir Aharoni
15 */
16
17 ( function ( mw ) {
18 /**
19 * Evaluates a plural rule in CLDR syntax for a number
20 * @param {string} rule
21 * @param {integer} number
22 * @return {boolean} true if evaluation passed, false if evaluation failed.
23 */
24
25 function pluralRuleParser(rule, number) {
26 'use strict';
27
28 /*
29 Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
30 -----------------------------------------------------------------
31 condition = and_condition ('or' and_condition)*
32 ('@integer' samples)?
33 ('@decimal' samples)?
34 and_condition = relation ('and' relation)*
35 relation = is_relation | in_relation | within_relation
36 is_relation = expr 'is' ('not')? value
37 in_relation = expr (('not')? 'in' | '=' | '!=') range_list
38 within_relation = expr ('not')? 'within' range_list
39 expr = operand (('mod' | '%') value)?
40 operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
41 range_list = (range | value) (',' range_list)*
42 value = digit+
43 digit = 0|1|2|3|4|5|6|7|8|9
44 range = value'..'value
45 samples = sampleRange (',' sampleRange)* (',' ('…'|'...'))?
46 sampleRange = decimalValue '~' decimalValue
47 decimalValue = value ('.' value)?
48 */
49
50 // We don't evaluate the samples section of the rule. Ignore it.
51 rule = rule.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
52
53 if (!rule.length) {
54 // Empty rule or 'other' rule.
55 return true;
56 }
57
58 // Indicates the current position in the rule as we parse through it.
59 // Shared among all parsing functions below.
60 var pos = 0,
61 operand,
62 expression,
63 relation,
64 result,
65 whitespace = makeRegexParser(/^\s+/),
66 value = makeRegexParser(/^\d+/),
67 _n_ = makeStringParser('n'),
68 _i_ = makeStringParser('i'),
69 _f_ = makeStringParser('f'),
70 _t_ = makeStringParser('t'),
71 _v_ = makeStringParser('v'),
72 _w_ = makeStringParser('w'),
73 _is_ = makeStringParser('is'),
74 _isnot_ = makeStringParser('is not'),
75 _isnot_sign_ = makeStringParser('!='),
76 _equal_ = makeStringParser('='),
77 _mod_ = makeStringParser('mod'),
78 _percent_ = makeStringParser('%'),
79 _not_ = makeStringParser('not'),
80 _in_ = makeStringParser('in'),
81 _within_ = makeStringParser('within'),
82 _range_ = makeStringParser('..'),
83 _comma_ = makeStringParser(','),
84 _or_ = makeStringParser('or'),
85 _and_ = makeStringParser('and');
86
87 function debug() {
88 // console.log.apply(console, arguments);
89 }
90
91 debug('pluralRuleParser', rule, number);
92
93 // Try parsers until one works, if none work return null
94 function choice(parserSyntax) {
95 return function() {
96 var i, result;
97
98 for (i = 0; i < parserSyntax.length; i++) {
99 result = parserSyntax[i]();
100
101 if (result !== null) {
102 return result;
103 }
104 }
105
106 return null;
107 };
108 }
109
110 // Try several parserSyntax-es in a row.
111 // All must succeed; otherwise, return null.
112 // This is the only eager one.
113 function sequence(parserSyntax) {
114 var i, parserRes,
115 originalPos = pos,
116 result = [];
117
118 for (i = 0; i < parserSyntax.length; i++) {
119 parserRes = parserSyntax[i]();
120
121 if (parserRes === null) {
122 pos = originalPos;
123
124 return null;
125 }
126
127 result.push(parserRes);
128 }
129
130 return result;
131 }
132
133 // Run the same parser over and over until it fails.
134 // Must succeed a minimum of n times; otherwise, return null.
135 function nOrMore(n, p) {
136 return function() {
137 var originalPos = pos,
138 result = [],
139 parsed = p();
140
141 while (parsed !== null) {
142 result.push(parsed);
143 parsed = p();
144 }
145
146 if (result.length < n) {
147 pos = originalPos;
148
149 return null;
150 }
151
152 return result;
153 };
154 }
155
156 // Helpers - just make parserSyntax out of simpler JS builtin types
157 function makeStringParser(s) {
158 var len = s.length;
159
160 return function() {
161 var result = null;
162
163 if (rule.substr(pos, len) === s) {
164 result = s;
165 pos += len;
166 }
167
168 return result;
169 };
170 }
171
172 function makeRegexParser(regex) {
173 return function() {
174 var matches = rule.substr(pos).match(regex);
175
176 if (matches === null) {
177 return null;
178 }
179
180 pos += matches[0].length;
181
182 return matches[0];
183 };
184 }
185
186 /**
187 * Integer digits of n.
188 */
189 function i() {
190 var result = _i_();
191
192 if (result === null) {
193 debug(' -- failed i', parseInt(number, 10));
194
195 return result;
196 }
197
198 result = parseInt(number, 10);
199 debug(' -- passed i ', result);
200
201 return result;
202 }
203
204 /**
205 * Absolute value of the source number (integer and decimals).
206 */
207 function n() {
208 var result = _n_();
209
210 if (result === null) {
211 debug(' -- failed n ', number);
212
213 return result;
214 }
215
216 result = parseFloat(number, 10);
217 debug(' -- passed n ', result);
218
219 return result;
220 }
221
222 /**
223 * Visible fractional digits in n, with trailing zeros.
224 */
225 function f() {
226 var result = _f_();
227
228 if (result === null) {
229 debug(' -- failed f ', number);
230
231 return result;
232 }
233
234 result = (number + '.').split('.')[1] || 0;
235 debug(' -- passed f ', result);
236
237 return result;
238 }
239
240 /**
241 * Visible fractional digits in n, without trailing zeros.
242 */
243 function t() {
244 var result = _t_();
245
246 if (result === null) {
247 debug(' -- failed t ', number);
248
249 return result;
250 }
251
252 result = (number + '.').split('.')[1].replace(/0$/, '') || 0;
253 debug(' -- passed t ', result);
254
255 return result;
256 }
257
258 /**
259 * Number of visible fraction digits in n, with trailing zeros.
260 */
261 function v() {
262 var result = _v_();
263
264 if (result === null) {
265 debug(' -- failed v ', number);
266
267 return result;
268 }
269
270 result = (number + '.').split('.')[1].length || 0;
271 debug(' -- passed v ', result);
272
273 return result;
274 }
275
276 /**
277 * Number of visible fraction digits in n, without trailing zeros.
278 */
279 function w() {
280 var result = _w_();
281
282 if (result === null) {
283 debug(' -- failed w ', number);
284
285 return result;
286 }
287
288 result = (number + '.').split('.')[1].replace(/0$/, '').length || 0;
289 debug(' -- passed w ', result);
290
291 return result;
292 }
293
294 // operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
295 operand = choice([n, i, f, t, v, w]);
296
297 // expr = operand (('mod' | '%') value)?
298 expression = choice([mod, operand]);
299
300 function mod() {
301 var result = sequence(
302 [operand, whitespace, choice([_mod_, _percent_]), whitespace, value]
303 );
304
305 if (result === null) {
306 debug(' -- failed mod');
307
308 return null;
309 }
310
311 debug(' -- passed ' + parseInt(result[0], 10) + ' ' + result[2] + ' ' + parseInt(result[4], 10));
312
313 return parseInt(result[0], 10) % parseInt(result[4], 10);
314 }
315
316 function not() {
317 var result = sequence([whitespace, _not_]);
318
319 if (result === null) {
320 debug(' -- failed not');
321
322 return null;
323 }
324
325 return result[1];
326 }
327
328 // is_relation = expr 'is' ('not')? value
329 function is() {
330 var result = sequence([expression, whitespace, choice([_is_]), whitespace, value]);
331
332 if (result !== null) {
333 debug(' -- passed is : ' + result[0] + ' == ' + parseInt(result[4], 10));
334
335 return result[0] === parseInt(result[4], 10);
336 }
337
338 debug(' -- failed is');
339
340 return null;
341 }
342
343 // is_relation = expr 'is' ('not')? value
344 function isnot() {
345 var result = sequence(
346 [expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]
347 );
348
349 if (result !== null) {
350 debug(' -- passed isnot: ' + result[0] + ' != ' + parseInt(result[4], 10));
351
352 return result[0] !== parseInt(result[4], 10);
353 }
354
355 debug(' -- failed isnot');
356
357 return null;
358 }
359
360 function not_in() {
361 var i, range_list,
362 result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]);
363
364 if (result !== null) {
365 debug(' -- passed not_in: ' + result[0] + ' != ' + result[4]);
366 range_list = result[4];
367
368 for (i = 0; i < range_list.length; i++) {
369 if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
370 return false;
371 }
372 }
373
374 return true;
375 }
376
377 debug(' -- failed not_in');
378
379 return null;
380 }
381
382 // range_list = (range | value) (',' range_list)*
383 function rangeList() {
384 var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]),
385 resultList = [];
386
387 if (result !== null) {
388 resultList = resultList.concat(result[0]);
389
390 if (result[1][0]) {
391 resultList = resultList.concat(result[1][0]);
392 }
393
394 return resultList;
395 }
396
397 debug(' -- failed rangeList');
398
399 return null;
400 }
401
402 function rangeTail() {
403 // ',' range_list
404 var result = sequence([_comma_, rangeList]);
405
406 if (result !== null) {
407 return result[1];
408 }
409
410 debug(' -- failed rangeTail');
411
412 return null;
413 }
414
415 // range = value'..'value
416 function range() {
417 var i, array, left, right,
418 result = sequence([value, _range_, value]);
419
420 if (result !== null) {
421 debug(' -- passed range');
422
423 array = [];
424 left = parseInt(result[0], 10);
425 right = parseInt(result[2], 10);
426
427 for (i = left; i <= right; i++) {
428 array.push(i);
429 }
430
431 return array;
432 }
433
434 debug(' -- failed range');
435
436 return null;
437 }
438
439 function _in() {
440 var result, range_list, i;
441
442 // in_relation = expr ('not')? 'in' range_list
443 result = sequence(
444 [expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]
445 );
446
447 if (result !== null) {
448 debug(' -- passed _in:' + result);
449
450 range_list = result[5];
451
452 for (i = 0; i < range_list.length; i++) {
453 if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
454 return (result[1][0] !== 'not');
455 }
456 }
457
458 return (result[1][0] === 'not');
459 }
460
461 debug(' -- failed _in ');
462
463 return null;
464 }
465
466 /**
467 * The difference between "in" and "within" is that
468 * "in" only includes integers in the specified range,
469 * while "within" includes all values.
470 */
471 function within() {
472 var range_list, result;
473
474 // within_relation = expr ('not')? 'within' range_list
475 result = sequence(
476 [expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]
477 );
478
479 if (result !== null) {
480 debug(' -- passed within');
481
482 range_list = result[5];
483
484 if ((result[0] >= parseInt(range_list[0], 10)) &&
485 (result[0] < parseInt(range_list[range_list.length - 1], 10))) {
486
487 return (result[1][0] !== 'not');
488 }
489
490 return (result[1][0] === 'not');
491 }
492
493 debug(' -- failed within ');
494
495 return null;
496 }
497
498 // relation = is_relation | in_relation | within_relation
499 relation = choice([is, not_in, isnot, _in, within]);
500
501 // and_condition = relation ('and' relation)*
502 function and() {
503 var i,
504 result = sequence([relation, nOrMore(0, andTail)]);
505
506 if (result) {
507 if (!result[0]) {
508 return false;
509 }
510
511 for (i = 0; i < result[1].length; i++) {
512 if (!result[1][i]) {
513 return false;
514 }
515 }
516
517 return true;
518 }
519
520 debug(' -- failed and');
521
522 return null;
523 }
524
525 // ('and' relation)*
526 function andTail() {
527 var result = sequence([whitespace, _and_, whitespace, relation]);
528
529 if (result !== null) {
530 debug(' -- passed andTail' + result);
531
532 return result[3];
533 }
534
535 debug(' -- failed andTail');
536
537 return null;
538
539 }
540 // ('or' and_condition)*
541 function orTail() {
542 var result = sequence([whitespace, _or_, whitespace, and]);
543
544 if (result !== null) {
545 debug(' -- passed orTail: ' + result[3]);
546
547 return result[3];
548 }
549
550 debug(' -- failed orTail');
551
552 return null;
553 }
554
555 // condition = and_condition ('or' and_condition)*
556 function condition() {
557 var i,
558 result = sequence([and, nOrMore(0, orTail)]);
559
560 if (result) {
561 for (i = 0; i < result[1].length; i++) {
562 if (result[1][i]) {
563 return true;
564 }
565 }
566
567 return result[0];
568 }
569
570 return false;
571 }
572
573 result = condition();
574
575 /**
576 * For success, the pos must have gotten to the end of the rule
577 * and returned a non-null.
578 * n.b. This is part of language infrastructure,
579 * so we do not throw an internationalizable message.
580 */
581 if (result === null) {
582 throw new Error('Parse error at position ' + pos.toString() + ' for rule: ' + rule);
583 }
584
585 if (pos !== rule.length) {
586 debug('Warning: Rule not parsed completely. Parser stopped at ' + rule.substr(0, pos) + ' for rule: ' + rule);
587 }
588
589 return result;
590 }
591
592 /* pluralRuleParser ends here */
593 mw.libs.pluralRuleParser = pluralRuleParser;
594 module.exports = pluralRuleParser;
595
596 } )( mediaWiki );