dom/phonenumberutils/PhoneNumber.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/phonenumberutils/PhoneNumber.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,379 @@
     1.4 +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
     1.6 +
     1.7 +// Don't modify this code. Please use:
     1.8 +// https://github.com/andreasgal/PhoneNumber.js
     1.9 +
    1.10 +"use strict";
    1.11 +
    1.12 +this.EXPORTED_SYMBOLS = ["PhoneNumber"];
    1.13 +
    1.14 +Components.utils.import("resource://gre/modules/PhoneNumberMetaData.jsm");
    1.15 +Components.utils.import("resource://gre/modules/PhoneNumberNormalizer.jsm");
    1.16 +
    1.17 +this.PhoneNumber = (function (dataBase) {
    1.18 +  // Use strict in our context only - users might not want it
    1.19 +  'use strict';
    1.20 +
    1.21 +  const MAX_PHONE_NUMBER_LENGTH = 50;
    1.22 +  const NON_ALPHA_CHARS = /[^a-zA-Z]/g;
    1.23 +  const NON_DIALABLE_CHARS = /[^,#+\*\d]/g;
    1.24 +  const NON_DIALABLE_CHARS_ONCE = new RegExp(NON_DIALABLE_CHARS.source);
    1.25 +  const BACKSLASH = /\\/g;
    1.26 +  const SPLIT_FIRST_GROUP = /^(\d+)(.*)$/;
    1.27 +  const LEADING_PLUS_CHARS_PATTERN = /^[+\uFF0B]+/g;
    1.28 +
    1.29 +  // Format of the string encoded meta data. If the name contains "^" or "$"
    1.30 +  // we will generate a regular expression from the value, with those special
    1.31 +  // characters as prefix/suffix.
    1.32 +  const META_DATA_ENCODING = ["region",
    1.33 +                              "^(?:internationalPrefix)",
    1.34 +                              "nationalPrefix",
    1.35 +                              "^(?:nationalPrefixForParsing)",
    1.36 +                              "nationalPrefixTransformRule",
    1.37 +                              "nationalPrefixFormattingRule",
    1.38 +                              "^possiblePattern$",
    1.39 +                              "^nationalPattern$",
    1.40 +                              "formats"];
    1.41 +
    1.42 +  const FORMAT_ENCODING = ["^pattern$",
    1.43 +                           "nationalFormat",
    1.44 +                           "^leadingDigits",
    1.45 +                           "nationalPrefixFormattingRule",
    1.46 +                           "internationalFormat"];
    1.47 +
    1.48 +  var regionCache = Object.create(null);
    1.49 +
    1.50 +  // Parse an array of strings into a convenient object. We store meta
    1.51 +  // data as arrays since thats much more compact than JSON.
    1.52 +  function ParseArray(array, encoding, obj) {
    1.53 +    for (var n = 0; n < encoding.length; ++n) {
    1.54 +      var value = array[n];
    1.55 +      if (!value)
    1.56 +        continue;
    1.57 +      var field = encoding[n];
    1.58 +      var fieldAlpha = field.replace(NON_ALPHA_CHARS, "");
    1.59 +      if (field != fieldAlpha)
    1.60 +        value = new RegExp(field.replace(fieldAlpha, value));
    1.61 +      obj[fieldAlpha] = value;
    1.62 +    }
    1.63 +    return obj;
    1.64 +  }
    1.65 +
    1.66 +  // Parse string encoded meta data into a convenient object
    1.67 +  // representation.
    1.68 +  function ParseMetaData(countryCode, md) {
    1.69 +    var array = eval(md.replace(BACKSLASH, "\\\\"));
    1.70 +    md = ParseArray(array,
    1.71 +                    META_DATA_ENCODING,
    1.72 +                    { countryCode: countryCode });
    1.73 +    regionCache[md.region] = md;
    1.74 +    return md;
    1.75 +  }
    1.76 +
    1.77 +  // Parse string encoded format data into a convenient object
    1.78 +  // representation.
    1.79 +  function ParseFormat(md) {
    1.80 +    var formats = md.formats;
    1.81 +    if (!formats) {
    1.82 +      return null;
    1.83 +    }
    1.84 +    // Bail if we already parsed the format definitions.
    1.85 +    if (!(Array.isArray(formats[0])))
    1.86 +      return;
    1.87 +    for (var n = 0; n < formats.length; ++n) {
    1.88 +      formats[n] = ParseArray(formats[n],
    1.89 +                              FORMAT_ENCODING,
    1.90 +                              {});
    1.91 +    }
    1.92 +  }
    1.93 +
    1.94 +  // Search for the meta data associated with a region identifier ("US") in
    1.95 +  // our database, which is indexed by country code ("1"). Since we have
    1.96 +  // to walk the entire database for this, we cache the result of the lookup
    1.97 +  // for future reference.
    1.98 +  function FindMetaDataForRegion(region) {
    1.99 +    // Check in the region cache first. This will find all entries we have
   1.100 +    // already resolved (parsed from a string encoding).
   1.101 +    var md = regionCache[region];
   1.102 +    if (md)
   1.103 +      return md;
   1.104 +    for (var countryCode in dataBase) {
   1.105 +      var entry = dataBase[countryCode];
   1.106 +      // Each entry is a string encoded object of the form '["US..', or
   1.107 +      // an array of strings. We don't want to parse the string here
   1.108 +      // to save memory, so we just substring the region identifier
   1.109 +      // and compare it. For arrays, we compare against all region
   1.110 +      // identifiers with that country code. We skip entries that are
   1.111 +      // of type object, because they were already resolved (parsed into
   1.112 +      // an object), and their country code should have been in the cache.
   1.113 +      if (Array.isArray(entry)) {
   1.114 +        for (var n = 0; n < entry.length; n++) {
   1.115 +          if (typeof entry[n] == "string" && entry[n].substr(2,2) == region) {
   1.116 +            if (n > 0) {
   1.117 +              // Only the first entry has the formats field set.
   1.118 +              // Parse the main country if we haven't already and use
   1.119 +              // the formats field from the main country.
   1.120 +              if (typeof entry[0] == "string")
   1.121 +                entry[0] = ParseMetaData(countryCode, entry[0]);
   1.122 +              let formats = entry[0].formats;
   1.123 +              let current = ParseMetaData(countryCode, entry[n]);
   1.124 +              current.formats = formats;
   1.125 +              return entry[n] = current;
   1.126 +            }
   1.127 +
   1.128 +            entry[n] = ParseMetaData(countryCode, entry[n]);
   1.129 +            return entry[n];
   1.130 +          }
   1.131 +        }
   1.132 +        continue;
   1.133 +      }
   1.134 +      if (typeof entry == "string" && entry.substr(2,2) == region)
   1.135 +        return dataBase[countryCode] = ParseMetaData(countryCode, entry);
   1.136 +    }
   1.137 +  }
   1.138 +
   1.139 +  // Format a national number for a given region. The boolean flag "intl"
   1.140 +  // indicates whether we want the national or international format.
   1.141 +  function FormatNumber(regionMetaData, number, intl) {
   1.142 +    // We lazily parse the format description in the meta data for the region,
   1.143 +    // so make sure to parse it now if we haven't already done so.
   1.144 +    ParseFormat(regionMetaData);
   1.145 +    var formats = regionMetaData.formats;
   1.146 +    if (!formats) {
   1.147 +      return null;
   1.148 +    }
   1.149 +    for (var n = 0; n < formats.length; ++n) {
   1.150 +      var format = formats[n];
   1.151 +      // The leading digits field is optional. If we don't have it, just
   1.152 +      // use the matching pattern to qualify numbers.
   1.153 +      if (format.leadingDigits && !format.leadingDigits.test(number))
   1.154 +        continue;
   1.155 +      if (!format.pattern.test(number))
   1.156 +        continue;
   1.157 +      if (intl) {
   1.158 +        // If there is no international format, just fall back to the national
   1.159 +        // format.
   1.160 +        var internationalFormat = format.internationalFormat;
   1.161 +        if (!internationalFormat)
   1.162 +          internationalFormat = format.nationalFormat;
   1.163 +        // Some regions have numbers that can't be dialed from outside the
   1.164 +        // country, indicated by "NA" for the international format of that
   1.165 +        // number format pattern.
   1.166 +        if (internationalFormat == "NA")
   1.167 +          return null;
   1.168 +        // Prepend "+" and the country code.
   1.169 +        number = "+" + regionMetaData.countryCode + " " +
   1.170 +                 number.replace(format.pattern, internationalFormat);
   1.171 +      } else {
   1.172 +        number = number.replace(format.pattern, format.nationalFormat);
   1.173 +        // The region has a national prefix formatting rule, and it can be overwritten
   1.174 +        // by each actual number format rule.
   1.175 +        var nationalPrefixFormattingRule = regionMetaData.nationalPrefixFormattingRule;
   1.176 +        if (format.nationalPrefixFormattingRule)
   1.177 +          nationalPrefixFormattingRule = format.nationalPrefixFormattingRule;
   1.178 +        if (nationalPrefixFormattingRule) {
   1.179 +          // The prefix formatting rule contains two magic markers, "$NP" and "$FG".
   1.180 +          // "$NP" will be replaced by the national prefix, and "$FG" with the
   1.181 +          // first group of numbers.
   1.182 +          var match = number.match(SPLIT_FIRST_GROUP);
   1.183 +          if (match) {
   1.184 +            var firstGroup = match[1];
   1.185 +            var rest = match[2];
   1.186 +            var prefix = nationalPrefixFormattingRule;
   1.187 +            prefix = prefix.replace("$NP", regionMetaData.nationalPrefix);
   1.188 +            prefix = prefix.replace("$FG", firstGroup);
   1.189 +            number = prefix + rest;
   1.190 +          }
   1.191 +        }
   1.192 +      }
   1.193 +      return (number == "NA") ? null : number;
   1.194 +    }
   1.195 +    return null;
   1.196 +  }
   1.197 +
   1.198 +  function NationalNumber(regionMetaData, number) {
   1.199 +    this.region = regionMetaData.region;
   1.200 +    this.regionMetaData = regionMetaData;
   1.201 +    this.nationalNumber = number;
   1.202 +  }
   1.203 +
   1.204 +  // NationalNumber represents the result of parsing a phone number. We have
   1.205 +  // three getters on the prototype that format the number in national and
   1.206 +  // international format. Once called, the getters put a direct property
   1.207 +  // onto the object, caching the result.
   1.208 +  NationalNumber.prototype = {
   1.209 +    // +1 949-726-2896
   1.210 +    get internationalFormat() {
   1.211 +      var value = FormatNumber(this.regionMetaData, this.nationalNumber, true);
   1.212 +      Object.defineProperty(this, "internationalFormat", { value: value, enumerable: true });
   1.213 +      return value;
   1.214 +    },
   1.215 +    // (949) 726-2896
   1.216 +    get nationalFormat() {
   1.217 +      var value = FormatNumber(this.regionMetaData, this.nationalNumber, false);
   1.218 +      Object.defineProperty(this, "nationalFormat", { value: value, enumerable: true });
   1.219 +      return value;
   1.220 +    },
   1.221 +    // +19497262896
   1.222 +    get internationalNumber() {
   1.223 +      var value = this.internationalFormat ? this.internationalFormat.replace(NON_DIALABLE_CHARS, "")
   1.224 +                                           : null;
   1.225 +      Object.defineProperty(this, "internationalNumber", { value: value, enumerable: true });
   1.226 +      return value;
   1.227 +    },
   1.228 +    // country name 'US'
   1.229 +    get countryName() {
   1.230 +      var value = this.region ? this.region : null;
   1.231 +      Object.defineProperty(this, "countryName", { value: value, enumerable: true });
   1.232 +      return value;
   1.233 +    }
   1.234 +  };
   1.235 +
   1.236 +  // Check whether the number is valid for the given region.
   1.237 +  function IsValidNumber(number, md) {
   1.238 +    return md.possiblePattern.test(number);
   1.239 +  }
   1.240 +
   1.241 +  // Check whether the number is a valid national number for the given region.
   1.242 +  function IsNationalNumber(number, md) {
   1.243 +    return IsValidNumber(number, md) && md.nationalPattern.test(number);
   1.244 +  }
   1.245 +
   1.246 +  // Determine the country code a number starts with, or return null if
   1.247 +  // its not a valid country code.
   1.248 +  function ParseCountryCode(number) {
   1.249 +    for (var n = 1; n <= 3; ++n) {
   1.250 +      var cc = number.substr(0,n);
   1.251 +      if (dataBase[cc])
   1.252 +        return cc;
   1.253 +    }
   1.254 +    return null;
   1.255 +  }
   1.256 +
   1.257 +  // Parse an international number that starts with the country code. Return
   1.258 +  // null if the number is not a valid international number.
   1.259 +  function ParseInternationalNumber(number) {
   1.260 +    var ret;
   1.261 +
   1.262 +    // Parse and strip the country code.
   1.263 +    var countryCode = ParseCountryCode(number);
   1.264 +    if (!countryCode)
   1.265 +      return null;
   1.266 +    number = number.substr(countryCode.length);
   1.267 +
   1.268 +    // Lookup the meta data for the region (or regions) and if the rest of
   1.269 +    // the number parses for that region, return the parsed number.
   1.270 +    var entry = dataBase[countryCode];
   1.271 +    if (Array.isArray(entry)) {
   1.272 +      for (var n = 0; n < entry.length; ++n) {
   1.273 +        if (typeof entry[n] == "string")
   1.274 +          entry[n] = ParseMetaData(countryCode, entry[n]);
   1.275 +        if (n > 0)
   1.276 +          entry[n].formats = entry[0].formats;
   1.277 +        ret = ParseNationalNumber(number, entry[n])
   1.278 +        if (ret)
   1.279 +          return ret;
   1.280 +      }
   1.281 +      return null;
   1.282 +    }
   1.283 +    if (typeof entry == "string")
   1.284 +      entry = dataBase[countryCode] = ParseMetaData(countryCode, entry);
   1.285 +    return ParseNationalNumber(number, entry);
   1.286 +  }
   1.287 +
   1.288 +  // Parse a national number for a specific region. Return null if the
   1.289 +  // number is not a valid national number (it might still be a possible
   1.290 +  // number for parts of that region).
   1.291 +  function ParseNationalNumber(number, md) {
   1.292 +    if (!md.possiblePattern.test(number) ||
   1.293 +        !md.nationalPattern.test(number)) {
   1.294 +      return null;
   1.295 +    }
   1.296 +    // Success.
   1.297 +    return new NationalNumber(md, number);
   1.298 +  }
   1.299 +
   1.300 +  // Parse a number and transform it into the national format, removing any
   1.301 +  // international dial prefixes and country codes.
   1.302 +  function ParseNumber(number, defaultRegion) {
   1.303 +    var ret;
   1.304 +
   1.305 +    // Remove formating characters and whitespace.
   1.306 +    number = PhoneNumberNormalizer.Normalize(number);
   1.307 +
   1.308 +    // If there is no defaultRegion, we can't parse international access codes.
   1.309 +    if (!defaultRegion && number[0] !== '+')
   1.310 +      return null;
   1.311 +
   1.312 +    // Detect and strip leading '+'.
   1.313 +    if (number[0] === '+')
   1.314 +      return ParseInternationalNumber(number.replace(LEADING_PLUS_CHARS_PATTERN, ""));
   1.315 +
   1.316 +    // Lookup the meta data for the given region.
   1.317 +    var md = FindMetaDataForRegion(defaultRegion.toUpperCase());
   1.318 +
   1.319 +    // See if the number starts with an international prefix, and if the
   1.320 +    // number resulting from stripping the code is valid, then remove the
   1.321 +    // prefix and flag the number as international.
   1.322 +    if (md.internationalPrefix.test(number)) {
   1.323 +      var possibleNumber = number.replace(md.internationalPrefix, "");
   1.324 +      ret = ParseInternationalNumber(possibleNumber)
   1.325 +      if (ret)
   1.326 +        return ret;
   1.327 +    }
   1.328 +
   1.329 +    // This is not an international number. See if its a national one for
   1.330 +    // the current region. National numbers can start with the national
   1.331 +    // prefix, or without.
   1.332 +    if (md.nationalPrefixForParsing) {
   1.333 +      // Some regions have specific national prefix parse rules. Apply those.
   1.334 +      var withoutPrefix = number.replace(md.nationalPrefixForParsing,
   1.335 +                                         md.nationalPrefixTransformRule || '');
   1.336 +      ret = ParseNationalNumber(withoutPrefix, md)
   1.337 +      if (ret)
   1.338 +        return ret;
   1.339 +    } else {
   1.340 +      // If there is no specific national prefix rule, just strip off the
   1.341 +      // national prefix from the beginning of the number (if there is one).
   1.342 +      var nationalPrefix = md.nationalPrefix;
   1.343 +      if (nationalPrefix && number.indexOf(nationalPrefix) == 0 &&
   1.344 +          (ret = ParseNationalNumber(number.substr(nationalPrefix.length), md))) {
   1.345 +        return ret;
   1.346 +      }
   1.347 +    }
   1.348 +    ret = ParseNationalNumber(number, md)
   1.349 +    if (ret)
   1.350 +      return ret;
   1.351 +
   1.352 +    // Now lets see if maybe its an international number after all, but
   1.353 +    // without '+' or the international prefix.
   1.354 +    ret = ParseInternationalNumber(number)
   1.355 +    if (ret)
   1.356 +      return ret;
   1.357 +
   1.358 +    // If the number matches the possible numbers of the current region,
   1.359 +    // return it as a possible number.
   1.360 +    if (md.possiblePattern.test(number))
   1.361 +      return new NationalNumber(md, number);
   1.362 +
   1.363 +    // We couldn't parse the number at all.
   1.364 +    return null;
   1.365 +  }
   1.366 +
   1.367 +  function IsPlainPhoneNumber(number) {
   1.368 +    if (typeof number !== 'string') {
   1.369 +      return false;
   1.370 +    }
   1.371 +
   1.372 +    var length = number.length;
   1.373 +    var isTooLong = (length > MAX_PHONE_NUMBER_LENGTH);
   1.374 +    var isEmpty = (length === 0);
   1.375 +    return !(isTooLong || isEmpty || NON_DIALABLE_CHARS_ONCE.test(number));
   1.376 +  }
   1.377 +
   1.378 +  return {
   1.379 +    IsPlain: IsPlainPhoneNumber,
   1.380 +    Parse: ParseNumber,
   1.381 +  };
   1.382 +})(PHONE_NUMBER_META_DATA);

mercurial