michael@0: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ michael@0: michael@0: // Don't modify this code. Please use: michael@0: // https://github.com/andreasgal/PhoneNumber.js michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["PhoneNumber"]; michael@0: michael@0: Components.utils.import("resource://gre/modules/PhoneNumberMetaData.jsm"); michael@0: Components.utils.import("resource://gre/modules/PhoneNumberNormalizer.jsm"); michael@0: michael@0: this.PhoneNumber = (function (dataBase) { michael@0: // Use strict in our context only - users might not want it michael@0: 'use strict'; michael@0: michael@0: const MAX_PHONE_NUMBER_LENGTH = 50; michael@0: const NON_ALPHA_CHARS = /[^a-zA-Z]/g; michael@0: const NON_DIALABLE_CHARS = /[^,#+\*\d]/g; michael@0: const NON_DIALABLE_CHARS_ONCE = new RegExp(NON_DIALABLE_CHARS.source); michael@0: const BACKSLASH = /\\/g; michael@0: const SPLIT_FIRST_GROUP = /^(\d+)(.*)$/; michael@0: const LEADING_PLUS_CHARS_PATTERN = /^[+\uFF0B]+/g; michael@0: michael@0: // Format of the string encoded meta data. If the name contains "^" or "$" michael@0: // we will generate a regular expression from the value, with those special michael@0: // characters as prefix/suffix. michael@0: const META_DATA_ENCODING = ["region", michael@0: "^(?:internationalPrefix)", michael@0: "nationalPrefix", michael@0: "^(?:nationalPrefixForParsing)", michael@0: "nationalPrefixTransformRule", michael@0: "nationalPrefixFormattingRule", michael@0: "^possiblePattern$", michael@0: "^nationalPattern$", michael@0: "formats"]; michael@0: michael@0: const FORMAT_ENCODING = ["^pattern$", michael@0: "nationalFormat", michael@0: "^leadingDigits", michael@0: "nationalPrefixFormattingRule", michael@0: "internationalFormat"]; michael@0: michael@0: var regionCache = Object.create(null); michael@0: michael@0: // Parse an array of strings into a convenient object. We store meta michael@0: // data as arrays since thats much more compact than JSON. michael@0: function ParseArray(array, encoding, obj) { michael@0: for (var n = 0; n < encoding.length; ++n) { michael@0: var value = array[n]; michael@0: if (!value) michael@0: continue; michael@0: var field = encoding[n]; michael@0: var fieldAlpha = field.replace(NON_ALPHA_CHARS, ""); michael@0: if (field != fieldAlpha) michael@0: value = new RegExp(field.replace(fieldAlpha, value)); michael@0: obj[fieldAlpha] = value; michael@0: } michael@0: return obj; michael@0: } michael@0: michael@0: // Parse string encoded meta data into a convenient object michael@0: // representation. michael@0: function ParseMetaData(countryCode, md) { michael@0: var array = eval(md.replace(BACKSLASH, "\\\\")); michael@0: md = ParseArray(array, michael@0: META_DATA_ENCODING, michael@0: { countryCode: countryCode }); michael@0: regionCache[md.region] = md; michael@0: return md; michael@0: } michael@0: michael@0: // Parse string encoded format data into a convenient object michael@0: // representation. michael@0: function ParseFormat(md) { michael@0: var formats = md.formats; michael@0: if (!formats) { michael@0: return null; michael@0: } michael@0: // Bail if we already parsed the format definitions. michael@0: if (!(Array.isArray(formats[0]))) michael@0: return; michael@0: for (var n = 0; n < formats.length; ++n) { michael@0: formats[n] = ParseArray(formats[n], michael@0: FORMAT_ENCODING, michael@0: {}); michael@0: } michael@0: } michael@0: michael@0: // Search for the meta data associated with a region identifier ("US") in michael@0: // our database, which is indexed by country code ("1"). Since we have michael@0: // to walk the entire database for this, we cache the result of the lookup michael@0: // for future reference. michael@0: function FindMetaDataForRegion(region) { michael@0: // Check in the region cache first. This will find all entries we have michael@0: // already resolved (parsed from a string encoding). michael@0: var md = regionCache[region]; michael@0: if (md) michael@0: return md; michael@0: for (var countryCode in dataBase) { michael@0: var entry = dataBase[countryCode]; michael@0: // Each entry is a string encoded object of the form '["US..', or michael@0: // an array of strings. We don't want to parse the string here michael@0: // to save memory, so we just substring the region identifier michael@0: // and compare it. For arrays, we compare against all region michael@0: // identifiers with that country code. We skip entries that are michael@0: // of type object, because they were already resolved (parsed into michael@0: // an object), and their country code should have been in the cache. michael@0: if (Array.isArray(entry)) { michael@0: for (var n = 0; n < entry.length; n++) { michael@0: if (typeof entry[n] == "string" && entry[n].substr(2,2) == region) { michael@0: if (n > 0) { michael@0: // Only the first entry has the formats field set. michael@0: // Parse the main country if we haven't already and use michael@0: // the formats field from the main country. michael@0: if (typeof entry[0] == "string") michael@0: entry[0] = ParseMetaData(countryCode, entry[0]); michael@0: let formats = entry[0].formats; michael@0: let current = ParseMetaData(countryCode, entry[n]); michael@0: current.formats = formats; michael@0: return entry[n] = current; michael@0: } michael@0: michael@0: entry[n] = ParseMetaData(countryCode, entry[n]); michael@0: return entry[n]; michael@0: } michael@0: } michael@0: continue; michael@0: } michael@0: if (typeof entry == "string" && entry.substr(2,2) == region) michael@0: return dataBase[countryCode] = ParseMetaData(countryCode, entry); michael@0: } michael@0: } michael@0: michael@0: // Format a national number for a given region. The boolean flag "intl" michael@0: // indicates whether we want the national or international format. michael@0: function FormatNumber(regionMetaData, number, intl) { michael@0: // We lazily parse the format description in the meta data for the region, michael@0: // so make sure to parse it now if we haven't already done so. michael@0: ParseFormat(regionMetaData); michael@0: var formats = regionMetaData.formats; michael@0: if (!formats) { michael@0: return null; michael@0: } michael@0: for (var n = 0; n < formats.length; ++n) { michael@0: var format = formats[n]; michael@0: // The leading digits field is optional. If we don't have it, just michael@0: // use the matching pattern to qualify numbers. michael@0: if (format.leadingDigits && !format.leadingDigits.test(number)) michael@0: continue; michael@0: if (!format.pattern.test(number)) michael@0: continue; michael@0: if (intl) { michael@0: // If there is no international format, just fall back to the national michael@0: // format. michael@0: var internationalFormat = format.internationalFormat; michael@0: if (!internationalFormat) michael@0: internationalFormat = format.nationalFormat; michael@0: // Some regions have numbers that can't be dialed from outside the michael@0: // country, indicated by "NA" for the international format of that michael@0: // number format pattern. michael@0: if (internationalFormat == "NA") michael@0: return null; michael@0: // Prepend "+" and the country code. michael@0: number = "+" + regionMetaData.countryCode + " " + michael@0: number.replace(format.pattern, internationalFormat); michael@0: } else { michael@0: number = number.replace(format.pattern, format.nationalFormat); michael@0: // The region has a national prefix formatting rule, and it can be overwritten michael@0: // by each actual number format rule. michael@0: var nationalPrefixFormattingRule = regionMetaData.nationalPrefixFormattingRule; michael@0: if (format.nationalPrefixFormattingRule) michael@0: nationalPrefixFormattingRule = format.nationalPrefixFormattingRule; michael@0: if (nationalPrefixFormattingRule) { michael@0: // The prefix formatting rule contains two magic markers, "$NP" and "$FG". michael@0: // "$NP" will be replaced by the national prefix, and "$FG" with the michael@0: // first group of numbers. michael@0: var match = number.match(SPLIT_FIRST_GROUP); michael@0: if (match) { michael@0: var firstGroup = match[1]; michael@0: var rest = match[2]; michael@0: var prefix = nationalPrefixFormattingRule; michael@0: prefix = prefix.replace("$NP", regionMetaData.nationalPrefix); michael@0: prefix = prefix.replace("$FG", firstGroup); michael@0: number = prefix + rest; michael@0: } michael@0: } michael@0: } michael@0: return (number == "NA") ? null : number; michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: function NationalNumber(regionMetaData, number) { michael@0: this.region = regionMetaData.region; michael@0: this.regionMetaData = regionMetaData; michael@0: this.nationalNumber = number; michael@0: } michael@0: michael@0: // NationalNumber represents the result of parsing a phone number. We have michael@0: // three getters on the prototype that format the number in national and michael@0: // international format. Once called, the getters put a direct property michael@0: // onto the object, caching the result. michael@0: NationalNumber.prototype = { michael@0: // +1 949-726-2896 michael@0: get internationalFormat() { michael@0: var value = FormatNumber(this.regionMetaData, this.nationalNumber, true); michael@0: Object.defineProperty(this, "internationalFormat", { value: value, enumerable: true }); michael@0: return value; michael@0: }, michael@0: // (949) 726-2896 michael@0: get nationalFormat() { michael@0: var value = FormatNumber(this.regionMetaData, this.nationalNumber, false); michael@0: Object.defineProperty(this, "nationalFormat", { value: value, enumerable: true }); michael@0: return value; michael@0: }, michael@0: // +19497262896 michael@0: get internationalNumber() { michael@0: var value = this.internationalFormat ? this.internationalFormat.replace(NON_DIALABLE_CHARS, "") michael@0: : null; michael@0: Object.defineProperty(this, "internationalNumber", { value: value, enumerable: true }); michael@0: return value; michael@0: }, michael@0: // country name 'US' michael@0: get countryName() { michael@0: var value = this.region ? this.region : null; michael@0: Object.defineProperty(this, "countryName", { value: value, enumerable: true }); michael@0: return value; michael@0: } michael@0: }; michael@0: michael@0: // Check whether the number is valid for the given region. michael@0: function IsValidNumber(number, md) { michael@0: return md.possiblePattern.test(number); michael@0: } michael@0: michael@0: // Check whether the number is a valid national number for the given region. michael@0: function IsNationalNumber(number, md) { michael@0: return IsValidNumber(number, md) && md.nationalPattern.test(number); michael@0: } michael@0: michael@0: // Determine the country code a number starts with, or return null if michael@0: // its not a valid country code. michael@0: function ParseCountryCode(number) { michael@0: for (var n = 1; n <= 3; ++n) { michael@0: var cc = number.substr(0,n); michael@0: if (dataBase[cc]) michael@0: return cc; michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: // Parse an international number that starts with the country code. Return michael@0: // null if the number is not a valid international number. michael@0: function ParseInternationalNumber(number) { michael@0: var ret; michael@0: michael@0: // Parse and strip the country code. michael@0: var countryCode = ParseCountryCode(number); michael@0: if (!countryCode) michael@0: return null; michael@0: number = number.substr(countryCode.length); michael@0: michael@0: // Lookup the meta data for the region (or regions) and if the rest of michael@0: // the number parses for that region, return the parsed number. michael@0: var entry = dataBase[countryCode]; michael@0: if (Array.isArray(entry)) { michael@0: for (var n = 0; n < entry.length; ++n) { michael@0: if (typeof entry[n] == "string") michael@0: entry[n] = ParseMetaData(countryCode, entry[n]); michael@0: if (n > 0) michael@0: entry[n].formats = entry[0].formats; michael@0: ret = ParseNationalNumber(number, entry[n]) michael@0: if (ret) michael@0: return ret; michael@0: } michael@0: return null; michael@0: } michael@0: if (typeof entry == "string") michael@0: entry = dataBase[countryCode] = ParseMetaData(countryCode, entry); michael@0: return ParseNationalNumber(number, entry); michael@0: } michael@0: michael@0: // Parse a national number for a specific region. Return null if the michael@0: // number is not a valid national number (it might still be a possible michael@0: // number for parts of that region). michael@0: function ParseNationalNumber(number, md) { michael@0: if (!md.possiblePattern.test(number) || michael@0: !md.nationalPattern.test(number)) { michael@0: return null; michael@0: } michael@0: // Success. michael@0: return new NationalNumber(md, number); michael@0: } michael@0: michael@0: // Parse a number and transform it into the national format, removing any michael@0: // international dial prefixes and country codes. michael@0: function ParseNumber(number, defaultRegion) { michael@0: var ret; michael@0: michael@0: // Remove formating characters and whitespace. michael@0: number = PhoneNumberNormalizer.Normalize(number); michael@0: michael@0: // If there is no defaultRegion, we can't parse international access codes. michael@0: if (!defaultRegion && number[0] !== '+') michael@0: return null; michael@0: michael@0: // Detect and strip leading '+'. michael@0: if (number[0] === '+') michael@0: return ParseInternationalNumber(number.replace(LEADING_PLUS_CHARS_PATTERN, "")); michael@0: michael@0: // Lookup the meta data for the given region. michael@0: var md = FindMetaDataForRegion(defaultRegion.toUpperCase()); michael@0: michael@0: // See if the number starts with an international prefix, and if the michael@0: // number resulting from stripping the code is valid, then remove the michael@0: // prefix and flag the number as international. michael@0: if (md.internationalPrefix.test(number)) { michael@0: var possibleNumber = number.replace(md.internationalPrefix, ""); michael@0: ret = ParseInternationalNumber(possibleNumber) michael@0: if (ret) michael@0: return ret; michael@0: } michael@0: michael@0: // This is not an international number. See if its a national one for michael@0: // the current region. National numbers can start with the national michael@0: // prefix, or without. michael@0: if (md.nationalPrefixForParsing) { michael@0: // Some regions have specific national prefix parse rules. Apply those. michael@0: var withoutPrefix = number.replace(md.nationalPrefixForParsing, michael@0: md.nationalPrefixTransformRule || ''); michael@0: ret = ParseNationalNumber(withoutPrefix, md) michael@0: if (ret) michael@0: return ret; michael@0: } else { michael@0: // If there is no specific national prefix rule, just strip off the michael@0: // national prefix from the beginning of the number (if there is one). michael@0: var nationalPrefix = md.nationalPrefix; michael@0: if (nationalPrefix && number.indexOf(nationalPrefix) == 0 && michael@0: (ret = ParseNationalNumber(number.substr(nationalPrefix.length), md))) { michael@0: return ret; michael@0: } michael@0: } michael@0: ret = ParseNationalNumber(number, md) michael@0: if (ret) michael@0: return ret; michael@0: michael@0: // Now lets see if maybe its an international number after all, but michael@0: // without '+' or the international prefix. michael@0: ret = ParseInternationalNumber(number) michael@0: if (ret) michael@0: return ret; michael@0: michael@0: // If the number matches the possible numbers of the current region, michael@0: // return it as a possible number. michael@0: if (md.possiblePattern.test(number)) michael@0: return new NationalNumber(md, number); michael@0: michael@0: // We couldn't parse the number at all. michael@0: return null; michael@0: } michael@0: michael@0: function IsPlainPhoneNumber(number) { michael@0: if (typeof number !== 'string') { michael@0: return false; michael@0: } michael@0: michael@0: var length = number.length; michael@0: var isTooLong = (length > MAX_PHONE_NUMBER_LENGTH); michael@0: var isEmpty = (length === 0); michael@0: return !(isTooLong || isEmpty || NON_DIALABLE_CHARS_ONCE.test(number)); michael@0: } michael@0: michael@0: return { michael@0: IsPlain: IsPlainPhoneNumber, michael@0: Parse: ParseNumber, michael@0: }; michael@0: })(PHONE_NUMBER_META_DATA);