dom/phonenumberutils/PhoneNumber.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
     4 // Don't modify this code. Please use:
     5 // https://github.com/andreasgal/PhoneNumber.js
     7 "use strict";
     9 this.EXPORTED_SYMBOLS = ["PhoneNumber"];
    11 Components.utils.import("resource://gre/modules/PhoneNumberMetaData.jsm");
    12 Components.utils.import("resource://gre/modules/PhoneNumberNormalizer.jsm");
    14 this.PhoneNumber = (function (dataBase) {
    15   // Use strict in our context only - users might not want it
    16   'use strict';
    18   const MAX_PHONE_NUMBER_LENGTH = 50;
    19   const NON_ALPHA_CHARS = /[^a-zA-Z]/g;
    20   const NON_DIALABLE_CHARS = /[^,#+\*\d]/g;
    21   const NON_DIALABLE_CHARS_ONCE = new RegExp(NON_DIALABLE_CHARS.source);
    22   const BACKSLASH = /\\/g;
    23   const SPLIT_FIRST_GROUP = /^(\d+)(.*)$/;
    24   const LEADING_PLUS_CHARS_PATTERN = /^[+\uFF0B]+/g;
    26   // Format of the string encoded meta data. If the name contains "^" or "$"
    27   // we will generate a regular expression from the value, with those special
    28   // characters as prefix/suffix.
    29   const META_DATA_ENCODING = ["region",
    30                               "^(?:internationalPrefix)",
    31                               "nationalPrefix",
    32                               "^(?:nationalPrefixForParsing)",
    33                               "nationalPrefixTransformRule",
    34                               "nationalPrefixFormattingRule",
    35                               "^possiblePattern$",
    36                               "^nationalPattern$",
    37                               "formats"];
    39   const FORMAT_ENCODING = ["^pattern$",
    40                            "nationalFormat",
    41                            "^leadingDigits",
    42                            "nationalPrefixFormattingRule",
    43                            "internationalFormat"];
    45   var regionCache = Object.create(null);
    47   // Parse an array of strings into a convenient object. We store meta
    48   // data as arrays since thats much more compact than JSON.
    49   function ParseArray(array, encoding, obj) {
    50     for (var n = 0; n < encoding.length; ++n) {
    51       var value = array[n];
    52       if (!value)
    53         continue;
    54       var field = encoding[n];
    55       var fieldAlpha = field.replace(NON_ALPHA_CHARS, "");
    56       if (field != fieldAlpha)
    57         value = new RegExp(field.replace(fieldAlpha, value));
    58       obj[fieldAlpha] = value;
    59     }
    60     return obj;
    61   }
    63   // Parse string encoded meta data into a convenient object
    64   // representation.
    65   function ParseMetaData(countryCode, md) {
    66     var array = eval(md.replace(BACKSLASH, "\\\\"));
    67     md = ParseArray(array,
    68                     META_DATA_ENCODING,
    69                     { countryCode: countryCode });
    70     regionCache[md.region] = md;
    71     return md;
    72   }
    74   // Parse string encoded format data into a convenient object
    75   // representation.
    76   function ParseFormat(md) {
    77     var formats = md.formats;
    78     if (!formats) {
    79       return null;
    80     }
    81     // Bail if we already parsed the format definitions.
    82     if (!(Array.isArray(formats[0])))
    83       return;
    84     for (var n = 0; n < formats.length; ++n) {
    85       formats[n] = ParseArray(formats[n],
    86                               FORMAT_ENCODING,
    87                               {});
    88     }
    89   }
    91   // Search for the meta data associated with a region identifier ("US") in
    92   // our database, which is indexed by country code ("1"). Since we have
    93   // to walk the entire database for this, we cache the result of the lookup
    94   // for future reference.
    95   function FindMetaDataForRegion(region) {
    96     // Check in the region cache first. This will find all entries we have
    97     // already resolved (parsed from a string encoding).
    98     var md = regionCache[region];
    99     if (md)
   100       return md;
   101     for (var countryCode in dataBase) {
   102       var entry = dataBase[countryCode];
   103       // Each entry is a string encoded object of the form '["US..', or
   104       // an array of strings. We don't want to parse the string here
   105       // to save memory, so we just substring the region identifier
   106       // and compare it. For arrays, we compare against all region
   107       // identifiers with that country code. We skip entries that are
   108       // of type object, because they were already resolved (parsed into
   109       // an object), and their country code should have been in the cache.
   110       if (Array.isArray(entry)) {
   111         for (var n = 0; n < entry.length; n++) {
   112           if (typeof entry[n] == "string" && entry[n].substr(2,2) == region) {
   113             if (n > 0) {
   114               // Only the first entry has the formats field set.
   115               // Parse the main country if we haven't already and use
   116               // the formats field from the main country.
   117               if (typeof entry[0] == "string")
   118                 entry[0] = ParseMetaData(countryCode, entry[0]);
   119               let formats = entry[0].formats;
   120               let current = ParseMetaData(countryCode, entry[n]);
   121               current.formats = formats;
   122               return entry[n] = current;
   123             }
   125             entry[n] = ParseMetaData(countryCode, entry[n]);
   126             return entry[n];
   127           }
   128         }
   129         continue;
   130       }
   131       if (typeof entry == "string" && entry.substr(2,2) == region)
   132         return dataBase[countryCode] = ParseMetaData(countryCode, entry);
   133     }
   134   }
   136   // Format a national number for a given region. The boolean flag "intl"
   137   // indicates whether we want the national or international format.
   138   function FormatNumber(regionMetaData, number, intl) {
   139     // We lazily parse the format description in the meta data for the region,
   140     // so make sure to parse it now if we haven't already done so.
   141     ParseFormat(regionMetaData);
   142     var formats = regionMetaData.formats;
   143     if (!formats) {
   144       return null;
   145     }
   146     for (var n = 0; n < formats.length; ++n) {
   147       var format = formats[n];
   148       // The leading digits field is optional. If we don't have it, just
   149       // use the matching pattern to qualify numbers.
   150       if (format.leadingDigits && !format.leadingDigits.test(number))
   151         continue;
   152       if (!format.pattern.test(number))
   153         continue;
   154       if (intl) {
   155         // If there is no international format, just fall back to the national
   156         // format.
   157         var internationalFormat = format.internationalFormat;
   158         if (!internationalFormat)
   159           internationalFormat = format.nationalFormat;
   160         // Some regions have numbers that can't be dialed from outside the
   161         // country, indicated by "NA" for the international format of that
   162         // number format pattern.
   163         if (internationalFormat == "NA")
   164           return null;
   165         // Prepend "+" and the country code.
   166         number = "+" + regionMetaData.countryCode + " " +
   167                  number.replace(format.pattern, internationalFormat);
   168       } else {
   169         number = number.replace(format.pattern, format.nationalFormat);
   170         // The region has a national prefix formatting rule, and it can be overwritten
   171         // by each actual number format rule.
   172         var nationalPrefixFormattingRule = regionMetaData.nationalPrefixFormattingRule;
   173         if (format.nationalPrefixFormattingRule)
   174           nationalPrefixFormattingRule = format.nationalPrefixFormattingRule;
   175         if (nationalPrefixFormattingRule) {
   176           // The prefix formatting rule contains two magic markers, "$NP" and "$FG".
   177           // "$NP" will be replaced by the national prefix, and "$FG" with the
   178           // first group of numbers.
   179           var match = number.match(SPLIT_FIRST_GROUP);
   180           if (match) {
   181             var firstGroup = match[1];
   182             var rest = match[2];
   183             var prefix = nationalPrefixFormattingRule;
   184             prefix = prefix.replace("$NP", regionMetaData.nationalPrefix);
   185             prefix = prefix.replace("$FG", firstGroup);
   186             number = prefix + rest;
   187           }
   188         }
   189       }
   190       return (number == "NA") ? null : number;
   191     }
   192     return null;
   193   }
   195   function NationalNumber(regionMetaData, number) {
   196     this.region = regionMetaData.region;
   197     this.regionMetaData = regionMetaData;
   198     this.nationalNumber = number;
   199   }
   201   // NationalNumber represents the result of parsing a phone number. We have
   202   // three getters on the prototype that format the number in national and
   203   // international format. Once called, the getters put a direct property
   204   // onto the object, caching the result.
   205   NationalNumber.prototype = {
   206     // +1 949-726-2896
   207     get internationalFormat() {
   208       var value = FormatNumber(this.regionMetaData, this.nationalNumber, true);
   209       Object.defineProperty(this, "internationalFormat", { value: value, enumerable: true });
   210       return value;
   211     },
   212     // (949) 726-2896
   213     get nationalFormat() {
   214       var value = FormatNumber(this.regionMetaData, this.nationalNumber, false);
   215       Object.defineProperty(this, "nationalFormat", { value: value, enumerable: true });
   216       return value;
   217     },
   218     // +19497262896
   219     get internationalNumber() {
   220       var value = this.internationalFormat ? this.internationalFormat.replace(NON_DIALABLE_CHARS, "")
   221                                            : null;
   222       Object.defineProperty(this, "internationalNumber", { value: value, enumerable: true });
   223       return value;
   224     },
   225     // country name 'US'
   226     get countryName() {
   227       var value = this.region ? this.region : null;
   228       Object.defineProperty(this, "countryName", { value: value, enumerable: true });
   229       return value;
   230     }
   231   };
   233   // Check whether the number is valid for the given region.
   234   function IsValidNumber(number, md) {
   235     return md.possiblePattern.test(number);
   236   }
   238   // Check whether the number is a valid national number for the given region.
   239   function IsNationalNumber(number, md) {
   240     return IsValidNumber(number, md) && md.nationalPattern.test(number);
   241   }
   243   // Determine the country code a number starts with, or return null if
   244   // its not a valid country code.
   245   function ParseCountryCode(number) {
   246     for (var n = 1; n <= 3; ++n) {
   247       var cc = number.substr(0,n);
   248       if (dataBase[cc])
   249         return cc;
   250     }
   251     return null;
   252   }
   254   // Parse an international number that starts with the country code. Return
   255   // null if the number is not a valid international number.
   256   function ParseInternationalNumber(number) {
   257     var ret;
   259     // Parse and strip the country code.
   260     var countryCode = ParseCountryCode(number);
   261     if (!countryCode)
   262       return null;
   263     number = number.substr(countryCode.length);
   265     // Lookup the meta data for the region (or regions) and if the rest of
   266     // the number parses for that region, return the parsed number.
   267     var entry = dataBase[countryCode];
   268     if (Array.isArray(entry)) {
   269       for (var n = 0; n < entry.length; ++n) {
   270         if (typeof entry[n] == "string")
   271           entry[n] = ParseMetaData(countryCode, entry[n]);
   272         if (n > 0)
   273           entry[n].formats = entry[0].formats;
   274         ret = ParseNationalNumber(number, entry[n])
   275         if (ret)
   276           return ret;
   277       }
   278       return null;
   279     }
   280     if (typeof entry == "string")
   281       entry = dataBase[countryCode] = ParseMetaData(countryCode, entry);
   282     return ParseNationalNumber(number, entry);
   283   }
   285   // Parse a national number for a specific region. Return null if the
   286   // number is not a valid national number (it might still be a possible
   287   // number for parts of that region).
   288   function ParseNationalNumber(number, md) {
   289     if (!md.possiblePattern.test(number) ||
   290         !md.nationalPattern.test(number)) {
   291       return null;
   292     }
   293     // Success.
   294     return new NationalNumber(md, number);
   295   }
   297   // Parse a number and transform it into the national format, removing any
   298   // international dial prefixes and country codes.
   299   function ParseNumber(number, defaultRegion) {
   300     var ret;
   302     // Remove formating characters and whitespace.
   303     number = PhoneNumberNormalizer.Normalize(number);
   305     // If there is no defaultRegion, we can't parse international access codes.
   306     if (!defaultRegion && number[0] !== '+')
   307       return null;
   309     // Detect and strip leading '+'.
   310     if (number[0] === '+')
   311       return ParseInternationalNumber(number.replace(LEADING_PLUS_CHARS_PATTERN, ""));
   313     // Lookup the meta data for the given region.
   314     var md = FindMetaDataForRegion(defaultRegion.toUpperCase());
   316     // See if the number starts with an international prefix, and if the
   317     // number resulting from stripping the code is valid, then remove the
   318     // prefix and flag the number as international.
   319     if (md.internationalPrefix.test(number)) {
   320       var possibleNumber = number.replace(md.internationalPrefix, "");
   321       ret = ParseInternationalNumber(possibleNumber)
   322       if (ret)
   323         return ret;
   324     }
   326     // This is not an international number. See if its a national one for
   327     // the current region. National numbers can start with the national
   328     // prefix, or without.
   329     if (md.nationalPrefixForParsing) {
   330       // Some regions have specific national prefix parse rules. Apply those.
   331       var withoutPrefix = number.replace(md.nationalPrefixForParsing,
   332                                          md.nationalPrefixTransformRule || '');
   333       ret = ParseNationalNumber(withoutPrefix, md)
   334       if (ret)
   335         return ret;
   336     } else {
   337       // If there is no specific national prefix rule, just strip off the
   338       // national prefix from the beginning of the number (if there is one).
   339       var nationalPrefix = md.nationalPrefix;
   340       if (nationalPrefix && number.indexOf(nationalPrefix) == 0 &&
   341           (ret = ParseNationalNumber(number.substr(nationalPrefix.length), md))) {
   342         return ret;
   343       }
   344     }
   345     ret = ParseNationalNumber(number, md)
   346     if (ret)
   347       return ret;
   349     // Now lets see if maybe its an international number after all, but
   350     // without '+' or the international prefix.
   351     ret = ParseInternationalNumber(number)
   352     if (ret)
   353       return ret;
   355     // If the number matches the possible numbers of the current region,
   356     // return it as a possible number.
   357     if (md.possiblePattern.test(number))
   358       return new NationalNumber(md, number);
   360     // We couldn't parse the number at all.
   361     return null;
   362   }
   364   function IsPlainPhoneNumber(number) {
   365     if (typeof number !== 'string') {
   366       return false;
   367     }
   369     var length = number.length;
   370     var isTooLong = (length > MAX_PHONE_NUMBER_LENGTH);
   371     var isEmpty = (length === 0);
   372     return !(isTooLong || isEmpty || NON_DIALABLE_CHARS_ONCE.test(number));
   373   }
   375   return {
   376     IsPlain: IsPlainPhoneNumber,
   377     Parse: ParseNumber,
   378   };
   379 })(PHONE_NUMBER_META_DATA);

mercurial