1 /* 2 * IString.js - ilib string subclass definition 3 * 4 * Copyright © 2012-2015, 2018, 2021-2022 JEDLSoft 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 */ 19 20 // !data plurals 21 22 var ilib = require("../index.js"); 23 var Utils = require("./Utils.js"); 24 var MathUtils = require("./MathUtils.js"); 25 var Locale = require("./Locale.js"); 26 27 /** 28 * @class 29 * Create a new ilib string instance. This string inherits from and 30 * extends the Javascript String class. It can be 31 * used almost anywhere that a normal Javascript string is used, though in 32 * some instances you will need to call the {@link #toString} method when 33 * a built-in Javascript string is needed. The formatting methods are 34 * methods that are not in the intrinsic String class and are most useful 35 * when localizing strings in an app or web site in combination with 36 * the ResBundle class.<p> 37 * 38 * This class is named IString ("ilib string") so as not to conflict with the 39 * built-in Javascript String class. 40 * 41 * @constructor 42 * @param {string|IString=} string initialize this instance with this string 43 */ 44 var IString = function (string) { 45 if (typeof(string) === 'object') { 46 if (string instanceof IString) { 47 this.str = string.str; 48 } else { 49 this.str = string.toString(); 50 } 51 } else if (typeof(string) === 'string') { 52 this.str = String(string); // copy it 53 } else { 54 this.str = ""; 55 } 56 this.length = this.str.length; 57 this.cpLength = -1; 58 this.localeSpec = ilib.getLocale(); 59 }; 60 61 /** 62 * Return true if the given character is a Unicode surrogate character, 63 * either high or low. 64 * 65 * @private 66 * @static 67 * @param {string} ch character to check 68 * @return {boolean} true if the character is a surrogate 69 */ 70 IString._isSurrogate = function (ch) { 71 var n = ch.charCodeAt(0); 72 return ((n >= 0xDC00 && n <= 0xDFFF) || (n >= 0xD800 && n <= 0xDBFF)); 73 }; 74 75 // build in the English rule 76 IString.plurals_default = { 77 "one": { 78 "and": [ 79 { 80 "eq": [ 81 "i", 82 1 83 ] 84 }, 85 { 86 "eq": [ 87 "v", 88 0 89 ] 90 } 91 ] 92 } 93 }; 94 95 /** 96 * Convert a UCS-4 code point to a Javascript string. The codepoint can be any valid 97 * UCS-4 Unicode character, including supplementary characters. Standard Javascript 98 * only supports supplementary characters using the UTF-16 encoding, which has 99 * values in the range 0x0000-0xFFFF. String.fromCharCode() will only 100 * give you a string containing 16-bit characters, and will not properly convert 101 * the code point for a supplementary character (which has a value > 0xFFFF) into 102 * two UTF-16 surrogate characters. Instead, it will just just give you whatever 103 * single character happens to be the same as your code point modulo 0x10000, which 104 * is almost never what you want.<p> 105 * 106 * Similarly, that means if you use String.charCodeAt() 107 * you will only retrieve a 16-bit value, which may possibly be a single 108 * surrogate character that is part of a surrogate pair representing a character 109 * in the supplementary plane. It will not give you a code point. Use 110 * IString.codePointAt() to access code points in a string, or use 111 * an iterator to walk through the code points in a string. 112 * 113 * @static 114 * @param {number} codepoint UCS-4 code point to convert to a character 115 * @return {string} a string containing the character represented by the codepoint 116 */ 117 IString.fromCodePoint = function (codepoint) { 118 if (codepoint < 0x10000) { 119 return String.fromCharCode(codepoint); 120 } else { 121 var high = Math.floor(codepoint / 0x10000) - 1; 122 var low = codepoint & 0xFFFF; 123 124 return String.fromCharCode(0xD800 | ((high & 0x000F) << 6) | ((low & 0xFC00) >> 10)) + 125 String.fromCharCode(0xDC00 | (low & 0x3FF)); 126 } 127 }; 128 129 /** 130 * Convert the character or the surrogate pair at the given 131 * index into the intrinsic Javascript string to a Unicode 132 * UCS-4 code point. 133 * 134 * @static 135 * @param {string} str string to get the code point from 136 * @param {number} index index into the string 137 * @return {number} code point of the character at the 138 * given index into the string 139 */ 140 IString.toCodePoint = function(str, index) { 141 if (!str || str.length === 0) { 142 return -1; 143 } 144 var code = -1, high = str.charCodeAt(index); 145 if (high >= 0xD800 && high <= 0xDBFF) { 146 if (str.length > index+1) { 147 var low = str.charCodeAt(index+1); 148 if (low >= 0xDC00 && low <= 0xDFFF) { 149 code = (((high & 0x3C0) >> 6) + 1) << 16 | 150 (((high & 0x3F) << 10) | (low & 0x3FF)); 151 } 152 } 153 } else { 154 code = high; 155 } 156 157 return code; 158 }; 159 160 /** 161 * Load the plural the definitions of plurals for the locale. 162 * @param {boolean=} sync 163 * @param {Locale|string=} locale 164 * @param {Object=} loadParams 165 * @param {function(*)=} onLoad 166 */ 167 IString.loadPlurals = function (sync, locale, loadParams, onLoad) { 168 var loc; 169 if (locale) { 170 loc = (typeof(locale) === 'string') ? new Locale(locale) : locale; 171 } else { 172 loc = new Locale(ilib.getLocale()); 173 } 174 var spec = loc.getLanguage(); 175 Utils.loadData({ 176 name: "plurals.json", 177 object: "IString", 178 locale: loc, 179 sync: sync, 180 loadParams: loadParams, 181 callback: ilib.bind(this, function(plurals) { 182 plurals = plurals || IString.plurals_default; 183 if (onLoad && typeof(onLoad) === 'function') { 184 onLoad(plurals); 185 } 186 }) 187 }); 188 }; 189 190 /** 191 * @private 192 * @static 193 */ 194 IString._fncs = { 195 /** 196 * @private 197 * @param {Object} obj 198 * @return {string|undefined} 199 */ 200 firstProp: function (obj) { 201 for (var p in obj) { 202 if (p && obj[p]) { 203 return p; 204 } 205 } 206 return undefined; // should never get here 207 }, 208 209 /** 210 * @private 211 * @param {Object} obj 212 * @return {string|undefined} 213 */ 214 firstPropRule: function (obj) { 215 if (Object.prototype.toString.call(obj) === '[object Array]') { 216 return "inrange"; 217 } else if (Object.prototype.toString.call(obj) === '[object Object]') { 218 for (var p in obj) { 219 if (p && obj[p]) { 220 return p; 221 } 222 } 223 224 } 225 return undefined; // should never get here 226 }, 227 228 /** 229 * @private 230 * @param {Object} obj 231 * @param {number|Object} n 232 * @return {?} 233 */ 234 getValue: function (obj, n) { 235 if (typeof(obj) === 'object') { 236 var subrule = IString._fncs.firstPropRule(obj); 237 if (subrule === "inrange") { 238 return IString._fncs[subrule](obj, n); 239 } 240 return IString._fncs[subrule](obj[subrule], n); 241 } else if (typeof(obj) === 'string') { 242 if (typeof(n) === 'object'){ 243 return n[obj]; 244 } 245 return n; 246 } else { 247 return obj; 248 } 249 }, 250 251 /** 252 * @private 253 * @param {number|Object} n 254 * @param {Array.<number|Array.<number>>|Object} range 255 * @return {boolean} 256 */ 257 matchRangeContinuous: function(n, range) { 258 259 for (var num in range) { 260 if (typeof(num) !== 'undefined' && typeof(range[num]) !== 'undefined') { 261 var obj = range[num]; 262 if (typeof(obj) === 'number') { 263 if (n === range[num]) { 264 return true; 265 } else if (n >= range[0] && n <= range[1]) { 266 return true; 267 } 268 } else if (Object.prototype.toString.call(obj) === '[object Array]') { 269 if (n >= obj[0] && n <= obj[1]) { 270 return true; 271 } 272 } 273 } 274 } 275 return false; 276 }, 277 278 /** 279 * @private 280 * @param {*} number 281 * @return {Object} 282 */ 283 calculateNumberDigits: function(number) { 284 var numberToString = number.toString(); 285 var parts = []; 286 var numberDigits = {}; 287 var operandSymbol = {}; 288 289 var exponentialNum = number.toExponential(); 290 var exponentialIndex = exponentialNum.indexOf("e"); 291 if (exponentialIndex !== -1) { 292 operandSymbol.c = parseInt(exponentialNum[exponentialIndex+2]); 293 operandSymbol.e = parseInt(exponentialNum[exponentialIndex+2]); 294 } else { 295 operandSymbol.c = 0; 296 operandSymbol.e = 0; 297 } 298 299 if (numberToString.indexOf('.') !== -1) { //decimal 300 parts = numberToString.split('.', 2); 301 numberDigits.integerPart = parseInt(parts[0], 10); 302 numberDigits.decimalPartLength = parts[1].length; 303 numberDigits.decimalPart = parseInt(parts[1], 10); 304 305 operandSymbol.n = parseFloat(number); 306 operandSymbol.i = numberDigits.integerPart; 307 operandSymbol.v = numberDigits.decimalPartLength; 308 operandSymbol.w = numberDigits.decimalPartLength; 309 operandSymbol.f = numberDigits.decimalPart; 310 operandSymbol.t = numberDigits.decimalPart; 311 312 } else { 313 numberDigits.integerPart = number; 314 numberDigits.decimalPartLength = 0; 315 numberDigits.decimalPart = 0; 316 317 operandSymbol.n = parseInt(number, 10); 318 operandSymbol.i = numberDigits.integerPart; 319 operandSymbol.v = 0; 320 operandSymbol.w = 0; 321 operandSymbol.f = 0; 322 operandSymbol.t = 0; 323 324 } 325 return operandSymbol 326 }, 327 328 /** 329 * @private 330 * @param {number|Object} n 331 * @param {Array.<number|Array.<number>>|Object} range 332 * @return {boolean} 333 */ 334 matchRange: function(n, range) { 335 return IString._fncs.matchRangeContinuous(n, range); 336 }, 337 338 /** 339 * @private 340 * @param {Object} rule 341 * @param {number} n 342 * @return {boolean} 343 */ 344 is: function(rule, n) { 345 var left = IString._fncs.getValue(rule[0], n); 346 var right = IString._fncs.getValue(rule[1], n); 347 return left === right; 348 }, 349 350 /** 351 * @private 352 * @param {Object} rule 353 * @param {number} n 354 * @return {boolean} 355 */ 356 isnot: function(rule, n) { 357 return IString._fncs.getValue(rule[0], n) !== IString._fncs.getValue(rule[1], n); 358 }, 359 360 /** 361 * @private 362 * @param {Object} rule 363 * @param {number|Object} n 364 * @return {boolean} 365 */ 366 inrange: function(rule, n) { 367 if (typeof(rule[0]) === 'number') { 368 if(typeof(n) === 'object') { 369 return IString._fncs.matchRange(n.n,rule); 370 } 371 return IString._fncs.matchRange(n,rule); 372 } else if (typeof(rule[0]) === 'undefined') { 373 var subrule = IString._fncs.firstPropRule(rule); 374 return IString._fncs[subrule](rule[subrule], n); 375 } else { 376 return IString._fncs.matchRange(IString._fncs.getValue(rule[0], n), rule[1]); 377 } 378 }, 379 /** 380 * @private 381 * @param {Object} rule 382 * @param {number} n 383 * @return {boolean} 384 */ 385 notin: function(rule, n) { 386 return !IString._fncs.matchRange(IString._fncs.getValue(rule[0], n), rule[1]); 387 }, 388 389 /** 390 * @private 391 * @param {Object} rule 392 * @param {number} n 393 * @return {boolean} 394 */ 395 within: function(rule, n) { 396 return IString._fncs.matchRangeContinuous(IString._fncs.getValue(rule[0], n), rule[1]); 397 }, 398 399 /** 400 * @private 401 * @param {Object} rule 402 * @param {number} n 403 * @return {number} 404 */ 405 mod: function(rule, n) { 406 return MathUtils.mod(IString._fncs.getValue(rule[0], n), IString._fncs.getValue(rule[1], n)); 407 }, 408 409 /** 410 * @private 411 * @param {Object} rule 412 * @param {number} n 413 * @return {number} 414 */ 415 n: function(rule, n) { 416 return n; 417 }, 418 419 /** 420 * @private 421 * @param {Object} rule 422 * @param {number|Object} n 423 * @return {boolean} 424 */ 425 or: function(rule, n) { 426 var ruleLength = rule.length; 427 var result, i; 428 for (i=0; i < ruleLength; i++) { 429 result = IString._fncs.getValue(rule[i], n); 430 if (result) { 431 return true; 432 } 433 } 434 return false; 435 }, 436 /** 437 * @private 438 * @param {Object} rule 439 * @param {number|Object} n 440 * @return {boolean} 441 */ 442 and: function(rule, n) { 443 var ruleLength = rule.length; 444 var result, i; 445 for (i=0; i < ruleLength; i++) { 446 result= IString._fncs.getValue(rule[i], n); 447 if (!result) { 448 return false; 449 } 450 } 451 return true; 452 }, 453 /** 454 * @private 455 * @param {Object} rule 456 * @param {number|Object} n 457 * @return {boolean} 458 */ 459 eq: function(rule, n) { 460 var valueLeft = IString._fncs.getValue(rule[0], n); 461 var valueRight; 462 463 if (typeof(rule[0]) === 'string') { 464 if (typeof(n) === 'object'){ 465 valueRight = n[rule[0]]; 466 if (typeof(rule[1])=== 'number'){ 467 valueRight = IString._fncs.getValue(rule[1], n); 468 } else if (typeof(rule[1])=== 'object' && (IString._fncs.firstPropRule(rule[1]) === "inrange" )){ 469 valueRight = IString._fncs.getValue(rule[1], n); 470 } 471 } 472 } else { 473 if (IString._fncs.firstPropRule(rule[1]) === "inrange") { // mod 474 valueRight = IString._fncs.getValue(rule[1], valueLeft); 475 } else { 476 valueRight = IString._fncs.getValue(rule[1], n); 477 } 478 } 479 if(typeof(valueRight) === 'boolean') { 480 return (valueRight ? true : false); 481 } else { 482 return (valueLeft === valueRight ? true :false); 483 } 484 }, 485 /** 486 * @private 487 * @param {Object} rule 488 * @param {number|Object} n 489 * @return {boolean} 490 */ 491 neq: function(rule, n) { 492 var valueLeft = IString._fncs.getValue(rule[0], n); 493 var valueRight; 494 var leftRange; 495 var rightRange; 496 497 if (typeof(rule[0]) === 'string') { 498 valueRight = n[rule[0]]; 499 if (typeof(rule[1])=== 'number'){ 500 valueRight = IString._fncs.getValue(rule[1], n); 501 } else if (typeof(rule[1]) === 'object') { 502 leftRange = rule[1][0]; 503 rightRange = rule[1][1]; 504 if (typeof(leftRange) === 'number' && 505 typeof(rightRange) === 'number'){ 506 507 if (valueLeft >= leftRange && valueRight <= rightRange) { 508 return false 509 } else { 510 return true; 511 } 512 } 513 } 514 } else { 515 if (IString._fncs.firstPropRule(rule[1]) === "inrange") { // mod 516 valueRight = IString._fncs.getValue(rule[1], valueLeft); 517 } else { 518 valueRight = IString._fncs.getValue(rule[1], n); 519 } 520 } 521 522 if(typeof(valueRight) === 'boolean') {//mod 523 return (valueRight? false : true); 524 } else { 525 return (valueLeft !== valueRight ? true :false); 526 } 527 528 } 529 }; 530 531 IString.prototype = { 532 /** 533 * Return the length of this string in characters. This function defers to the regular 534 * Javascript string class in order to perform the length function. Please note that this 535 * method is a real method, whereas the length property of Javascript strings is 536 * implemented by native code and appears as a property.<p> 537 * 538 * Example: 539 * 540 * <pre> 541 * var str = new IString("this is a string"); 542 * console.log("String is " + str._length() + " characters long."); 543 * </pre> 544 * @private 545 * @deprecated 546 */ 547 _length: function () { 548 return this.str.length; 549 }, 550 551 /** 552 * Format this string instance as a message, replacing the parameters with 553 * the given values.<p> 554 * 555 * The string can contain any text that a regular Javascript string can 556 * contain. Replacement parameters have the syntax: 557 * 558 * <pre> 559 * {name} 560 * </pre> 561 * 562 * Where "name" can be any string surrounded by curly brackets. The value of 563 * "name" is taken from the parameters argument.<p> 564 * 565 * Example: 566 * 567 * <pre> 568 * var str = new IString("There are {num} objects."); 569 * console.log(str.format({ 570 * num: 12 571 * }); 572 * </pre> 573 * 574 * Would give the output: 575 * 576 * <pre> 577 * There are 12 objects. 578 * </pre> 579 * 580 * If a property is missing from the parameter block, the replacement 581 * parameter substring is left untouched in the string, and a different 582 * set of parameters may be applied a second time. This way, different 583 * parts of the code may format different parts of the message that they 584 * happen to know about.<p> 585 * 586 * Example: 587 * 588 * <pre> 589 * var str = new IString("There are {num} objects in the {container}."); 590 * console.log(str.format({ 591 * num: 12 592 * }); 593 * </pre> 594 * 595 * Would give the output:<p> 596 * 597 * <pre> 598 * There are 12 objects in the {container}. 599 * </pre> 600 * 601 * The result can then be formatted again with a different parameter block that 602 * specifies a value for the container property. 603 * 604 * @param params a Javascript object containing values for the replacement 605 * parameters in the current string 606 * @return a new IString instance with as many replacement parameters filled 607 * out as possible with real values. 608 */ 609 format: function (params) { 610 var formatted = this.str; 611 if (params) { 612 var regex; 613 for (var p in params) { 614 if (typeof(params[p]) !== 'undefined') { 615 regex = new RegExp("\{"+p+"\}", "g"); 616 formatted = formatted.replace(regex, params[p]); 617 } 618 } 619 } 620 return formatted.toString(); 621 }, 622 623 /** @private */ 624 _testChoice: function(index, limit) { 625 var operandValue = {}; 626 627 switch (typeof(index)) { 628 case 'number': 629 operandValue = IString._fncs.calculateNumberDigits(index); 630 631 if (limit.substring(0,2) === "<=") { 632 limit = parseFloat(limit.substring(2)); 633 return operandValue.n <= limit; 634 } else if (limit.substring(0,2) === ">=") { 635 limit = parseFloat(limit.substring(2)); 636 return operandValue.n >= limit; 637 } else if (limit.charAt(0) === "<") { 638 limit = parseFloat(limit.substring(1)); 639 return operandValue.n < limit; 640 } else if (limit.charAt(0) === ">") { 641 limit = parseFloat(limit.substring(1)); 642 return operandValue.n > limit; 643 } else { 644 this.locale = this.locale || new Locale(this.localeSpec); 645 switch (limit) { 646 case "zero": 647 case "one": 648 case "two": 649 case "few": 650 case "many": 651 // CLDR locale-dependent number classes 652 var ruleset = ilib.data["plurals_" + this.locale.getLanguage()+ "_" + this.locale.getRegion()] || ilib.data["plurals_" + this.locale.getLanguage()]|| IString.plurals_default; 653 if (ruleset) { 654 var rule = ruleset[limit]; 655 return IString._fncs.getValue(rule, operandValue); 656 } 657 break; 658 case "": 659 case "other": 660 // matches anything 661 return true; 662 default: 663 var dash = limit.indexOf("-"); 664 if (dash !== -1) { 665 // range 666 var start = limit.substring(0, dash); 667 var end = limit.substring(dash+1); 668 return operandValue.n >= parseInt(start, 10) && operandValue.n <= parseInt(end, 10); 669 } else { 670 return operandValue.n === parseInt(limit, 10); 671 } 672 } 673 } 674 break; 675 case 'boolean': 676 return (limit === "true" && index === true) || (limit === "false" && index === false); 677 678 case 'string': 679 var regexp = new RegExp(limit, "i"); 680 return regexp.test(index); 681 682 case 'object': 683 throw "syntax error: formatChoice parameter for the argument index cannot be an object"; 684 } 685 686 return false; 687 }, 688 /** @private */ 689 _isIntlPluralAvailable: function(locale) { 690 if (typeof (locale.getVariant()) !== 'undefined'){ 691 return false; 692 } 693 694 if (typeof(Intl) !== 'undefined') { 695 if (ilib._getPlatform() === 'nodejs') { 696 var version = process.versions["node"]; 697 if (!version) return false; 698 var majorVersion = version.split(".")[0]; 699 if (Number(majorVersion) >= 10 && (Intl.PluralRules.supportedLocalesOf(locale.getSpec()).length > 0)) { 700 return true; 701 } 702 return false; 703 } else if (Intl.PluralRules.supportedLocalesOf(locale.getSpec()).length > 0) { 704 return true; 705 } else { 706 return false; 707 } 708 } 709 return false; 710 }, 711 712 /** 713 * Format a string as one of a choice of strings dependent on the value of 714 * a particular argument index or array of indices.<p> 715 * 716 * The syntax of the choice string is as follows. The string contains a 717 * series of choices separated by a vertical bar character "|". Each choice 718 * has a value or range of values to match followed by a hash character "#" 719 * followed by the string to use if the variable matches the criteria.<p> 720 * 721 * Example string: 722 * 723 * <pre> 724 * var num = 2; 725 * var str = new IString("0#There are no objects.|1#There is one object.|2#There are {number} objects."); 726 * console.log(str.formatChoice(num, { 727 * number: num 728 * })); 729 * </pre> 730 * 731 * Gives the output: 732 * 733 * <pre> 734 * "There are 2 objects." 735 * </pre> 736 * 737 * The strings to format may contain replacement variables that will be formatted 738 * using the format() method above and the params argument as a source of values 739 * to use while formatting those variables.<p> 740 * 741 * If the criterion for a particular choice is empty, that choice will be used 742 * as the default one for use when none of the other choice's criteria match.<p> 743 * 744 * Example string: 745 * 746 * <pre> 747 * var num = 22; 748 * var str = new IString("0#There are no objects.|1#There is one object.|#There are {number} objects."); 749 * console.log(str.formatChoice(num, { 750 * number: num 751 * })); 752 * </pre> 753 * 754 * Gives the output: 755 * 756 * <pre> 757 * "There are 22 objects." 758 * </pre> 759 * 760 * If multiple choice patterns can match a given argument index, the first one 761 * encountered in the string will be used. If no choice patterns match the 762 * argument index, then the default choice will be used. If there is no default 763 * choice defined, then this method will return an empty string.<p> 764 * 765 * <b>Special Syntax</b><p> 766 * 767 * For any choice format string, all of the patterns in the string should be 768 * of a single type: numeric, boolean, or string/regexp. The type of the 769 * patterns is determined by the type of the argument index parameter.<p> 770 * 771 * If the argument index is numeric, then some special syntax can be used 772 * in the patterns to match numeric ranges.<p> 773 * 774 * <ul> 775 * <li><i>>x</i> - match any number that is greater than x 776 * <li><i>>=x</i> - match any number that is greater than or equal to x 777 * <li><i><x</i> - match any number that is less than x 778 * <li><i><=x</i> - match any number that is less than or equal to x 779 * <li><i>start-end</i> - match any number in the range [start,end) 780 * <li><i>zero</i> - match any number in the class "zero". (See below for 781 * a description of number classes.) 782 * <li><i>one</i> - match any number in the class "one" 783 * <li><i>two</i> - match any number in the class "two" 784 * <li><i>few</i> - match any number in the class "few" 785 * <li><i>many</i> - match any number in the class "many" 786 * <li><i>other</i> - match any number in the other or default class 787 * </ul> 788 * 789 * A number class defines a set of numbers that receive a particular syntax 790 * in the strings. For example, in Slovenian, integers ending in the digit 791 * "1" are in the "one" class, including 1, 21, 31, ... 101, 111, etc. 792 * Similarly, integers ending in the digit "2" are in the "two" class. 793 * Integers ending in the digits "3" or "4" are in the "few" class, and 794 * every other integer is handled by the default string.<p> 795 * 796 * The definition of what numbers are included in a class is locale-dependent. 797 * They are defined in the data file plurals.json. If your string is in a 798 * different locale than the default for ilib, you should call the setLocale() 799 * method of the string instance before calling this method.<p> 800 * 801 * <b>Other Pattern Types</b><p> 802 * 803 * If the argument index is a boolean, the string values "true" and "false" 804 * may appear as the choice patterns.<p> 805 * 806 * If the argument index is of type string, then the choice patterns may contain 807 * regular expressions, or static strings as degenerate regexps.<p> 808 * 809 * <b>Multiple Indexes</b><p> 810 * 811 * If you have 2 or more indexes to format into a string, you can pass them as 812 * an array. When you do that, the patterns to match should be a comma-separate 813 * list of patterns as per the rules above.<p> 814 * 815 * Example string: 816 * 817 * <pre> 818 * var str = new IString("zero,zero#There are no objects on zero pages.|one,one#There is 1 object on 1 page.|other,one#There are {number} objects on 1 page.|#There are {number} objects on {pages} pages."); 819 * var num = 4, pages = 1; 820 * console.log(str.formatChoice([num, pages], { 821 * number: num, 822 * pages: pages 823 * })); 824 * </pre> 825 * 826 * Gives the output:<p> 827 * 828 * <pre> 829 * "There are 4 objects on 1 page." 830 * </pre> 831 * 832 * Note that when there is a single index, you would typically leave the pattern blank to 833 * indicate the default choice. When there are multiple indices, sometimes one of the 834 * patterns has to be the default case when the other is not. Rather than leaving one or 835 * more of the patterns blank with commas that look out-of-place in the middle of it, you 836 * can use the word "other" to indicate a match with the default or other choice. The above example 837 * shows the use of the "other" pattern. That said, you are allowed to leave the pattern 838 * blank if you so choose. In the example above, the pattern for the third string could 839 * easily have been written as ",one" instead of "other,one" and the result will be the same. 840 * 841 * @param {*|Array.<*>} argIndex The index into the choice array of the current parameter, 842 * or an array of indices 843 * @param {Object} params The hash of parameter values that replace the replacement 844 * variables in the string 845 * * @param {boolean} useIntlPlural [optional] true if you are willing to use Intl.PluralRules object 846 * If it is omitted, the default value is true 847 * @throws "syntax error in choice format pattern: " if there is a syntax error 848 * @return {string} the formatted string 849 */ 850 formatChoice: function(argIndex, params, useIntlPlural) { 851 var choices = this.str.split("|"); 852 var limits = []; 853 var strings = []; 854 var limitsArr = []; 855 var i; 856 var parts; 857 var result = undefined; 858 var defaultCase = ""; 859 var checkArgsType; 860 var useIntl = typeof(useIntlPlural) !== 'undefined' ? useIntlPlural : true; 861 if (this.str.length === 0) { 862 // nothing to do 863 return ""; 864 } 865 866 // first parse all the choices 867 for (i = 0; i < choices.length; i++) { 868 parts = choices[i].split("#"); 869 if (parts.length > 2) { 870 limits[i] = parts[0]; 871 parts = parts.shift(); 872 strings[i] = parts.join("#"); 873 } else if (parts.length === 2) { 874 limits[i] = parts[0]; 875 strings[i] = parts[1]; 876 } else { 877 // syntax error 878 throw "syntax error in choice format pattern: " + choices[i]; 879 } 880 } 881 882 var args = (ilib.isArray(argIndex)) ? argIndex : [argIndex]; 883 884 checkArgsType = args.filter(ilib.bind(this, function(item){ 885 if (typeof(item) !== "number") { 886 return false; 887 } 888 return true; 889 })); 890 891 if (useIntl && this.intlPlural && (args.length === checkArgsType.length)){ 892 this.cateArr = []; 893 for(i = 0; i < args.length;i++) { 894 var r = this.intlPlural.select(args[i]); 895 this.cateArr.push(r); 896 } 897 if (args.length === 1) { 898 var idx = limits.indexOf(this.cateArr[0]); 899 if (idx == -1) { 900 idx = limits.indexOf(""); 901 } 902 result = new IString(strings[idx]); 903 } else { 904 if (limits.length === 0) { 905 defaultCase = new IString(strings[i]); 906 } else { 907 this.findOne = false; 908 909 for(i = 0; !this.findOne && i < limits.length; i++){ 910 limitsArr = (limits[i].indexOf(",") > -1) ? limits[i].split(",") : [limits[i]]; 911 912 if (limitsArr.length > 1 && (limitsArr.length < this.cateArr.length)){ 913 this.cateArr = this.cateArr.slice(0,limitsArr.length); 914 } 915 limitsArr = limitsArr.map(function(item){ 916 return item.trim(); 917 }) 918 limitsArr.filter(ilib.bind(this, function(element, idx, arr){ 919 if (JSON.stringify(arr) === JSON.stringify(this.cateArr)){ 920 this.number = i; 921 this.fineOne = true; 922 } 923 })); 924 } 925 if (this.number === -1){ 926 this.number = limits.indexOf(""); 927 } 928 result = new IString(strings[this.number]); 929 } 930 } 931 } else { 932 // then apply the argument index (or indices) 933 for (i = 0; i < limits.length; i++) { 934 if (limits[i].length === 0) { 935 // this is default case 936 defaultCase = new IString(strings[i]); 937 } else { 938 limitsArr = (limits[i].indexOf(",") > -1) ? limits[i].split(",") : [limits[i]]; 939 940 var applicable = true; 941 for (var j = 0; applicable && j < args.length && j < limitsArr.length; j++) { 942 applicable = this._testChoice(args[j], limitsArr[j]); 943 } 944 945 if (applicable) { 946 result = new IString(strings[i]); 947 i = limits.length; 948 } 949 } 950 } 951 } 952 if (!result) { 953 result = defaultCase || new IString(""); 954 } 955 956 result = result.format(params); 957 958 return result.toString(); 959 }, 960 961 // delegates 962 /** 963 * Same as String.toString() 964 * @return {string} this instance as regular Javascript string 965 */ 966 toString: function () { 967 return this.str.toString(); 968 }, 969 970 /** 971 * Same as String.valueOf() 972 * @return {string} this instance as a regular Javascript string 973 */ 974 valueOf: function () { 975 return this.str.valueOf(); 976 }, 977 978 /** 979 * Same as String.charAt() 980 * @param {number} index the index of the character being sought 981 * @return {IString} the character at the given index 982 */ 983 charAt: function(index) { 984 return new IString(this.str.charAt(index)); 985 }, 986 987 /** 988 * Same as String.charCodeAt(). This only reports on 989 * 2-byte UCS-2 Unicode values, and does not take into 990 * account supplementary characters encoded in UTF-16. 991 * If you would like to take account of those characters, 992 * use codePointAt() instead. 993 * @param {number} index the index of the character being sought 994 * @return {number} the character code of the character at the 995 * given index in the string 996 */ 997 charCodeAt: function(index) { 998 return this.str.charCodeAt(index); 999 }, 1000 1001 /** 1002 * Same as String.concat() 1003 * @param {string} strings strings to concatenate to the current one 1004 * @return {IString} a concatenation of the given strings 1005 */ 1006 concat: function(strings) { 1007 return new IString(this.str.concat(strings)); 1008 }, 1009 1010 /** 1011 * Same as String.indexOf() 1012 * @param {string} searchValue string to search for 1013 * @param {number} start index into the string to start searching, or 1014 * undefined to search the entire string 1015 * @return {number} index into the string of the string being sought, 1016 * or -1 if the string is not found 1017 */ 1018 indexOf: function(searchValue, start) { 1019 return this.str.indexOf(searchValue, start); 1020 }, 1021 1022 /** 1023 * Same as String.lastIndexOf() 1024 * @param {string} searchValue string to search for 1025 * @param {number} start index into the string to start searching, or 1026 * undefined to search the entire string 1027 * @return {number} index into the string of the string being sought, 1028 * or -1 if the string is not found 1029 */ 1030 lastIndexOf: function(searchValue, start) { 1031 return this.str.lastIndexOf(searchValue, start); 1032 }, 1033 1034 /** 1035 * Same as String.match() 1036 * @param {string} regexp the regular expression to match 1037 * @return {Array.<string>} an array of matches 1038 */ 1039 match: function(regexp) { 1040 return this.str.match(regexp); 1041 }, 1042 1043 /** 1044 * Same as String.matchAll() 1045 * @param {string} regexp the regular expression to match 1046 * @return {iterator} an iterator of the matches 1047 */ 1048 matchAll: function(regexp) { 1049 return this.str.matchAll(regexp); 1050 }, 1051 1052 /** 1053 * Same as String.replace() 1054 * @param {string} searchValue a regular expression to search for 1055 * @param {string} newValue the string to replace the matches with 1056 * @return {IString} a new string with all the matches replaced 1057 * with the new value 1058 */ 1059 replace: function(searchValue, newValue) { 1060 return new IString(this.str.replace(searchValue, newValue)); 1061 }, 1062 1063 /** 1064 * Same as String.search() 1065 * @param {string} regexp the regular expression to search for 1066 * @return {number} position of the match, or -1 for no match 1067 */ 1068 search: function(regexp) { 1069 return this.str.search(regexp); 1070 }, 1071 1072 /** 1073 * Same as String.slice() 1074 * @param {number} start first character to include in the string 1075 * @param {number} end include all characters up to, but not including 1076 * the end character 1077 * @return {IString} a slice of the current string 1078 */ 1079 slice: function(start, end) { 1080 return new IString(this.str.slice(start, end)); 1081 }, 1082 1083 /** 1084 * Same as String.split() 1085 * @param {string} separator regular expression to match to find 1086 * separations between the parts of the text 1087 * @param {number} limit maximum number of items in the final 1088 * output array. Any items beyond that limit will be ignored. 1089 * @return {Array.<string>} the parts of the current string split 1090 * by the separator 1091 */ 1092 split: function(separator, limit) { 1093 return this.str.split(separator, limit); 1094 }, 1095 1096 /** 1097 * Same as String.substr() 1098 * @param {number} start the index of the character that should 1099 * begin the returned substring 1100 * @param {number} length the number of characters to return after 1101 * the start character. 1102 * @return {IString} the requested substring 1103 */ 1104 substr: function(start, length) { 1105 var plat = ilib._getPlatform(); 1106 if (plat === "qt" || plat === "rhino" || plat === "trireme") { 1107 // qt and rhino have a broken implementation of substr(), so 1108 // work around it 1109 if (typeof(length) === "undefined") { 1110 length = this.str.length - start; 1111 } 1112 } 1113 return new IString(this.str.substr(start, length)); 1114 }, 1115 1116 /** 1117 * Same as String.substring() 1118 * @param {number} from the index of the character that should 1119 * begin the returned substring 1120 * @param {number} to the index where to stop the extraction. If 1121 * omitted, extracts the rest of the string 1122 * @return {IString} the requested substring 1123 */ 1124 substring: function(from, to) { 1125 return this.str.substring(from, to); 1126 }, 1127 1128 /** 1129 * Same as String.toLowerCase(). Note that this method is 1130 * not locale-sensitive. 1131 * @return {IString} a string with the first character 1132 * lower-cased 1133 */ 1134 toLowerCase: function() { 1135 return this.str.toLowerCase(); 1136 }, 1137 1138 /** 1139 * Same as String.toUpperCase(). Note that this method is 1140 * not locale-sensitive. Use toLocaleUpperCase() instead 1141 * to get locale-sensitive behaviour. 1142 * @return {IString} a string with the first character 1143 * upper-cased 1144 */ 1145 toUpperCase: function() { 1146 return this.str.toUpperCase(); 1147 }, 1148 1149 /** 1150 * Same as String.endsWith(). 1151 * @return {boolean} true if the given characters are found at 1152 * the end of the string, and false otherwise 1153 */ 1154 endsWith: function(searchString, length) { 1155 /* (note)length is optional. If it is omitted the default value is the length of string. 1156 * But If length is omitted, it returns false on QT. (tested on QT 5.12.4 and 5.13.0) 1157 */ 1158 if (typeof length === "undefined") { 1159 length = this.str.length; 1160 } 1161 return this.str.endsWith(searchString, length); 1162 }, 1163 1164 /** 1165 * Same as String.startsWith(). 1166 * @return {boolean} true if the given characters are found at 1167 * the beginning of the string, and false otherwise 1168 */ 1169 startsWith: function(searchString, length) { 1170 return this.str.startsWith(searchString, length); 1171 }, 1172 1173 /** 1174 * Same as String.includes(). 1175 * @return {boolean} true if the search string is found anywhere 1176 * with the given string, and false otherwise 1177 */ 1178 includes: function(searchString, position) { 1179 return this.str.includes(searchString, position); 1180 }, 1181 1182 /** 1183 * Same as String.normalize(). If this JS engine does not support 1184 * this method, then you can use the NormString class of ilib 1185 * to the same thing (albeit a little slower). 1186 * 1187 * @return {string} the string in normalized form 1188 */ 1189 normalize: function(form) { 1190 return this.str.normalize(form); 1191 }, 1192 1193 /** 1194 * Same as String.padEnd(). 1195 * @return {string} a string of the specified length with the 1196 * pad string applied at the end of the current string 1197 */ 1198 padEnd: function(targetLength, padString) { 1199 return this.str.padEnd(targetLength, padString); 1200 }, 1201 1202 /** 1203 * Same as String.padStart(). 1204 * @return {string} a string of the specified length with the 1205 * pad string applied at the end of the current string 1206 */ 1207 padStart: function(targetLength, padString) { 1208 return this.str.padStart(targetLength, padString); 1209 }, 1210 1211 /** 1212 * Same as String.repeat(). 1213 * @return {string} a new string containing the specified number 1214 * of copies of the given string 1215 */ 1216 repeat: function(count) { 1217 return this.str.repeat(count); 1218 }, 1219 1220 /** 1221 * Same as String.toLocaleLowerCase(). If the JS engine does not support this 1222 * method, you can use the ilib CaseMapper class instead. 1223 * @return {string} a new string representing the calling string 1224 * converted to lower case, according to any locale-sensitive 1225 * case mappings 1226 */ 1227 toLocaleLowerCase: function(locale) { 1228 return this.str.toLocaleLowerCase(locale); 1229 }, 1230 1231 /** 1232 * Same as String.toLocaleUpperCase(). If the JS engine does not support this 1233 * method, you can use the ilib CaseMapper class instead. 1234 * @return {string} a new string representing the calling string 1235 * converted to upper case, according to any locale-sensitive 1236 * case mappings 1237 */ 1238 toLocaleUpperCase: function(locale) { 1239 return this.str.toLocaleUpperCase(locale); 1240 }, 1241 1242 /** 1243 * Same as String.trim(). 1244 * @return {string} a new string representing the calling string stripped 1245 * of whitespace from both ends. 1246 */ 1247 trim: function() { 1248 return this.str.trim(); 1249 }, 1250 1251 /** 1252 * Same as String.trimEnd(). 1253 * @return {string} a new string representing the calling string stripped 1254 * of whitespace from its (right) end. 1255 */ 1256 trimEnd: function() { 1257 return this.str.trimEnd(); 1258 }, 1259 1260 /** 1261 * Same as String.trimRight(). 1262 * @return {string} a new string representing the calling string stripped 1263 * of whitespace from its (right) end. 1264 */ 1265 trimRight: function() { 1266 return this.str.trimRight(); 1267 }, 1268 1269 /** 1270 * Same as String.trimStart(). 1271 * @return {string} A new string representing the calling string stripped 1272 * of whitespace from its beginning (left end). 1273 */ 1274 trimStart: function() { 1275 return this.str.trimStart(); 1276 }, 1277 1278 /** 1279 * Same as String.trimLeft(). 1280 * @return {string} A new string representing the calling string stripped 1281 * of whitespace from its beginning (left end). 1282 */ 1283 trimLeft: function() { 1284 return this.str.trimLeft(); 1285 }, 1286 1287 /** 1288 * Convert the character or the surrogate pair at the given 1289 * index into the string to a Unicode UCS-4 code point. 1290 * @protected 1291 * @param {number} index index into the string 1292 * @return {number} code point of the character at the 1293 * given index into the string 1294 */ 1295 _toCodePoint: function (index) { 1296 return IString.toCodePoint(this.str, index); 1297 }, 1298 1299 /** 1300 * Call the callback with each character in the string one at 1301 * a time, taking care to step through the surrogate pairs in 1302 * the UTF-16 encoding properly.<p> 1303 * 1304 * The standard Javascript String's charAt() method only 1305 * returns a particular 16-bit character in the 1306 * UTF-16 encoding scheme. 1307 * If the index to charAt() is pointing to a low- or 1308 * high-surrogate character, 1309 * it will return the surrogate character rather 1310 * than the the character 1311 * in the supplementary planes that the two surrogates together 1312 * encode. This function will call the callback with the full 1313 * character, making sure to join two 1314 * surrogates into one character in the supplementary planes 1315 * where necessary.<p> 1316 * 1317 * @param {function(string)} callback a callback function to call with each 1318 * full character in the current string 1319 */ 1320 forEach: function(callback) { 1321 if (typeof(callback) === 'function') { 1322 var it = this.charIterator(); 1323 while (it.hasNext()) { 1324 callback(it.next()); 1325 } 1326 } 1327 }, 1328 1329 /** 1330 * Call the callback with each numeric code point in the string one at 1331 * a time, taking care to step through the surrogate pairs in 1332 * the UTF-16 encoding properly.<p> 1333 * 1334 * The standard Javascript String's charCodeAt() method only 1335 * returns information about a particular 16-bit character in the 1336 * UTF-16 encoding scheme. 1337 * If the index to charCodeAt() is pointing to a low- or 1338 * high-surrogate character, 1339 * it will return the code point of the surrogate character rather 1340 * than the code point of the character 1341 * in the supplementary planes that the two surrogates together 1342 * encode. This function will call the callback with the full 1343 * code point of each character, making sure to join two 1344 * surrogates into one code point in the supplementary planes.<p> 1345 * 1346 * @param {function(string)} callback a callback function to call with each 1347 * code point in the current string 1348 */ 1349 forEachCodePoint: function(callback) { 1350 if (typeof(callback) === 'function') { 1351 var it = this.iterator(); 1352 while (it.hasNext()) { 1353 callback(it.next()); 1354 } 1355 } 1356 }, 1357 1358 /** 1359 * Return an iterator that will step through all of the characters 1360 * in the string one at a time and return their code points, taking 1361 * care to step through the surrogate pairs in UTF-16 encoding 1362 * properly.<p> 1363 * 1364 * The standard Javascript String's charCodeAt() method only 1365 * returns information about a particular 16-bit character in the 1366 * UTF-16 encoding scheme. 1367 * If the index is pointing to a low- or high-surrogate character, 1368 * it will return a code point of the surrogate character rather 1369 * than the code point of the character 1370 * in the supplementary planes that the two surrogates together 1371 * encode.<p> 1372 * 1373 * The iterator instance returned has two methods, hasNext() which 1374 * returns true if the iterator has more code points to iterate through, 1375 * and next() which returns the next code point as a number.<p> 1376 * 1377 * @return {Object} an iterator 1378 * that iterates through all the code points in the string 1379 */ 1380 iterator: function() { 1381 /** 1382 * @constructor 1383 */ 1384 function _iterator (istring) { 1385 this.index = 0; 1386 this.hasNext = function () { 1387 return (this.index < istring.str.length); 1388 }; 1389 this.next = function () { 1390 if (this.index < istring.str.length) { 1391 var num = istring._toCodePoint(this.index); 1392 this.index += ((num > 0xFFFF) ? 2 : 1); 1393 } else { 1394 num = -1; 1395 } 1396 return num; 1397 }; 1398 }; 1399 return new _iterator(this); 1400 }, 1401 1402 /** 1403 * Return an iterator that will step through all of the characters 1404 * in the string one at a time, taking 1405 * care to step through the surrogate pairs in UTF-16 encoding 1406 * properly.<p> 1407 * 1408 * The standard Javascript String's charAt() method only 1409 * returns information about a particular 16-bit character in the 1410 * UTF-16 encoding scheme. 1411 * If the index is pointing to a low- or high-surrogate character, 1412 * it will return that surrogate character rather 1413 * than the surrogate pair which represents a character 1414 * in the supplementary planes.<p> 1415 * 1416 * The iterator instance returned has two methods, hasNext() which 1417 * returns true if the iterator has more characters to iterate through, 1418 * and next() which returns the next character.<p> 1419 * 1420 * @return {Object} an iterator 1421 * that iterates through all the characters in the string 1422 */ 1423 charIterator: function() { 1424 /** 1425 * @constructor 1426 */ 1427 function _chiterator (istring) { 1428 this.index = 0; 1429 this.hasNext = function () { 1430 return (this.index < istring.str.length); 1431 }; 1432 this.next = function () { 1433 var ch; 1434 if (this.index < istring.str.length) { 1435 ch = istring.str.charAt(this.index); 1436 if (IString._isSurrogate(ch) && 1437 this.index+1 < istring.str.length && 1438 IString._isSurrogate(istring.str.charAt(this.index+1))) { 1439 this.index++; 1440 ch += istring.str.charAt(this.index); 1441 } 1442 this.index++; 1443 } 1444 return ch; 1445 }; 1446 }; 1447 return new _chiterator(this); 1448 }, 1449 1450 /** 1451 * Return the code point at the given index when the string is viewed 1452 * as an array of code points. If the index is beyond the end of the 1453 * array of code points or if the index is negative, -1 is returned. 1454 * @param {number} index index of the code point 1455 * @return {number} code point of the character at the given index into 1456 * the string 1457 */ 1458 codePointAt: function (index) { 1459 if (index < 0) { 1460 return -1; 1461 } 1462 var count, 1463 it = this.iterator(), 1464 ch; 1465 for (count = index; count >= 0 && it.hasNext(); count--) { 1466 ch = it.next(); 1467 } 1468 return (count < 0) ? ch : -1; 1469 }, 1470 1471 /** 1472 * Set the locale to use when processing choice formats. The locale 1473 * affects how number classes are interpretted. In some cultures, 1474 * the limit "few" maps to "any integer that ends in the digits 2 to 9" and 1475 * in yet others, "few" maps to "any integer that ends in the digits 1476 * 3 or 4". 1477 * @param {Locale|string} locale locale to use when processing choice 1478 * formats with this string 1479 * @param {boolean=} sync [optional] whether to load the locale data synchronously 1480 * or not 1481 * @param {Object=} loadParams [optional] parameters to pass to the loader function 1482 * @param {function(*)=} onLoad [optional] function to call when the loading is done 1483 */ 1484 setLocale: function (locale, sync, loadParams, onLoad) { 1485 if (typeof(locale) === 'object') { 1486 this.locale = locale; 1487 } else { 1488 this.localeSpec = locale; 1489 this.locale = new Locale(locale); 1490 } 1491 1492 if (this._isIntlPluralAvailable(this.locale)){ 1493 this.intlPlural = new Intl.PluralRules(this.locale.getSpec()); 1494 } 1495 1496 IString.loadPlurals(typeof(sync) !== 'undefined' ? sync : true, this.locale, loadParams, onLoad); 1497 }, 1498 1499 /** 1500 * Return the locale to use when processing choice formats. The locale 1501 * affects how number classes are interpretted. In some cultures, 1502 * the limit "few" maps to "any integer that ends in the digits 2 to 9" and 1503 * in yet others, "few" maps to "any integer that ends in the digits 1504 * 3 or 4". 1505 * @return {string} localespec to use when processing choice 1506 * formats with this string 1507 */ 1508 getLocale: function () { 1509 return (this.locale ? this.locale.getSpec() : this.localeSpec) || ilib.getLocale(); 1510 }, 1511 1512 /** 1513 * Return the number of code points in this string. This may be different 1514 * than the number of characters, as the UTF-16 encoding that Javascript 1515 * uses for its basis returns surrogate pairs separately. Two 2-byte 1516 * surrogate characters together make up one character/code point in 1517 * the supplementary character planes. If your string contains no 1518 * characters in the supplementary planes, this method will return the 1519 * same thing as the length() method. 1520 * @return {number} the number of code points in this string 1521 */ 1522 codePointLength: function () { 1523 if (this.cpLength === -1) { 1524 var it = this.iterator(); 1525 this.cpLength = 0; 1526 while (it.hasNext()) { 1527 this.cpLength++; 1528 it.next(); 1529 }; 1530 } 1531 return this.cpLength; 1532 } 1533 }; 1534 1535 module.exports = IString; 1536