1 /* -*- indent-tabs-mode: nil; js-indent-level: 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
9 this.EXPORTED_SYMBOLS = ["PhoneNumber"];
11 const Cu = Components.utils;
13 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
14 XPCOMUtils.defineLazyModuleGetter(this, "PHONE_NUMBER_META_DATA",
15 "resource://gre/modules/PhoneNumberMetaData.jsm");
16 XPCOMUtils.defineLazyModuleGetter(this, "PhoneNumberNormalizer",
17 "resource://gre/modules/PhoneNumberNormalizer.jsm");
18 this.PhoneNumber = (function (dataBase) {
19 // Use strict in our context only - users might not want it
22 const MAX_PHONE_NUMBER_LENGTH = 50;
23 const NON_ALPHA_CHARS = /[^a-zA-Z]/g;
24 const NON_DIALABLE_CHARS = /[^,#+\*\d]/g;
25 const NON_DIALABLE_CHARS_ONCE = new RegExp(NON_DIALABLE_CHARS.source);
26 const BACKSLASH = /\\/g;
27 const SPLIT_FIRST_GROUP = /^(\d+)(.*)$/;
28 const LEADING_PLUS_CHARS_PATTERN = /^[+\uFF0B]+/g;
30 // Format of the string encoded meta data. If the name contains "^" or "$"
31 // we will generate a regular expression from the value, with those special
32 // characters as prefix/suffix.
33 const META_DATA_ENCODING = ["region",
34 "^(?:internationalPrefix)",
36 "^(?:nationalPrefixForParsing)",
37 "nationalPrefixTransformRule",
38 "nationalPrefixFormattingRule",
43 const FORMAT_ENCODING = ["^pattern$",
46 "nationalPrefixFormattingRule",
47 "internationalFormat"];
49 var regionCache = Object.create(null);
51 // Parse an array of strings into a convenient object. We store meta
52 // data as arrays since thats much more compact than JSON.
53 function ParseArray(array, encoding, obj) {
54 for (var n = 0; n < encoding.length; ++n) {
58 var field = encoding[n];
59 var fieldAlpha = field.replace(NON_ALPHA_CHARS, "");
60 if (field != fieldAlpha)
61 value = new RegExp(field.replace(fieldAlpha, value));
62 obj[fieldAlpha] = value;
67 // Parse string encoded meta data into a convenient object
69 function ParseMetaData(countryCode, md) {
70 var array = eval(md.replace(BACKSLASH, "\\\\"));
71 md = ParseArray(array,
73 { countryCode: countryCode });
74 regionCache[md.region] = md;
78 // Parse string encoded format data into a convenient object
80 function ParseFormat(md) {
81 var formats = md.formats;
85 // Bail if we already parsed the format definitions.
86 if (!(Array.isArray(formats[0])))
88 for (var n = 0; n < formats.length; ++n) {
89 formats[n] = ParseArray(formats[n],
95 // Search for the meta data associated with a region identifier ("US") in
96 // our database, which is indexed by country code ("1"). Since we have
97 // to walk the entire database for this, we cache the result of the lookup
98 // for future reference.
99 function FindMetaDataForRegion(region) {
100 // Check in the region cache first. This will find all entries we have
101 // already resolved (parsed from a string encoding).
102 var md = regionCache[region];
105 for (var countryCode in dataBase) {
106 var entry = dataBase[countryCode];
107 // Each entry is a string encoded object of the form '["US..', or
108 // an array of strings. We don't want to parse the string here
109 // to save memory, so we just substring the region identifier
110 // and compare it. For arrays, we compare against all region
111 // identifiers with that country code. We skip entries that are
112 // of type object, because they were already resolved (parsed into
113 // an object), and their country code should have been in the cache.
114 if (Array.isArray(entry)) {
115 for (var n = 0; n < entry.length; n++) {
116 if (typeof entry[n] == "string" && entry[n].substr(2,2) == region) {
118 // Only the first entry has the formats field set.
119 // Parse the main country if we haven't already and use
120 // the formats field from the main country.
121 if (typeof entry[0] == "string")
122 entry[0] = ParseMetaData(countryCode, entry[0]);
123 let formats = entry[0].formats;
124 let current = ParseMetaData(countryCode, entry[n]);
125 current.formats = formats;
126 return entry[n] = current;
129 entry[n] = ParseMetaData(countryCode, entry[n]);
135 if (typeof entry == "string" && entry.substr(2,2) == region)
136 return dataBase[countryCode] = ParseMetaData(countryCode, entry);
140 // Format a national number for a given region. The boolean flag "intl"
141 // indicates whether we want the national or international format.
142 function FormatNumber(regionMetaData, number, intl) {
143 // We lazily parse the format description in the meta data for the region,
144 // so make sure to parse it now if we haven't already done so.
145 ParseFormat(regionMetaData);
146 var formats = regionMetaData.formats;
150 for (var n = 0; n < formats.length; ++n) {
151 var format = formats[n];
152 // The leading digits field is optional. If we don't have it, just
153 // use the matching pattern to qualify numbers.
154 if (format.leadingDigits && !format.leadingDigits.test(number))
156 if (!format.pattern.test(number))
159 // If there is no international format, just fall back to the national
161 var internationalFormat = format.internationalFormat;
162 if (!internationalFormat)
163 internationalFormat = format.nationalFormat;
164 // Some regions have numbers that can't be dialed from outside the
165 // country, indicated by "NA" for the international format of that
166 // number format pattern.
167 if (internationalFormat == "NA")
169 // Prepend "+" and the country code.
170 number = "+" + regionMetaData.countryCode + " " +
171 number.replace(format.pattern, internationalFormat);
173 number = number.replace(format.pattern, format.nationalFormat);
174 // The region has a national prefix formatting rule, and it can be overwritten
175 // by each actual number format rule.
176 var nationalPrefixFormattingRule = regionMetaData.nationalPrefixFormattingRule;
177 if (format.nationalPrefixFormattingRule)
178 nationalPrefixFormattingRule = format.nationalPrefixFormattingRule;
179 if (nationalPrefixFormattingRule) {
180 // The prefix formatting rule contains two magic markers, "$NP" and "$FG".
181 // "$NP" will be replaced by the national prefix, and "$FG" with the
182 // first group of numbers.
183 var match = number.match(SPLIT_FIRST_GROUP);
185 var firstGroup = match[1];
187 var prefix = nationalPrefixFormattingRule;
188 prefix = prefix.replace("$NP", regionMetaData.nationalPrefix);
189 prefix = prefix.replace("$FG", firstGroup);
190 number = prefix + rest;
194 return (number == "NA") ? null : number;
199 function NationalNumber(regionMetaData, number) {
200 this.region = regionMetaData.region;
201 this.regionMetaData = regionMetaData;
202 this.nationalNumber = number;
205 // NationalNumber represents the result of parsing a phone number. We have
206 // three getters on the prototype that format the number in national and
207 // international format. Once called, the getters put a direct property
208 // onto the object, caching the result.
209 NationalNumber.prototype = {
211 get internationalFormat() {
212 var value = FormatNumber(this.regionMetaData, this.nationalNumber, true);
213 Object.defineProperty(this, "internationalFormat", { value: value, enumerable: true });
217 get nationalFormat() {
218 var value = FormatNumber(this.regionMetaData, this.nationalNumber, false);
219 Object.defineProperty(this, "nationalFormat", { value: value, enumerable: true });
223 get internationalNumber() {
224 var value = this.internationalFormat ? this.internationalFormat.replace(NON_DIALABLE_CHARS, "")
226 Object.defineProperty(this, "internationalNumber", { value: value, enumerable: true });
231 var value = this.region ? this.region : null;
232 Object.defineProperty(this, "countryName", { value: value, enumerable: true });
237 // Check whether the number is valid for the given region.
238 function IsValidNumber(number, md) {
239 return md.possiblePattern.test(number);
242 // Check whether the number is a valid national number for the given region.
243 function IsNationalNumber(number, md) {
244 return IsValidNumber(number, md) && md.nationalPattern.test(number);
247 // Determine the country code a number starts with, or return null if
248 // its not a valid country code.
249 function ParseCountryCode(number) {
250 for (var n = 1; n <= 3; ++n) {
251 var cc = number.substr(0,n);
258 // Parse an international number that starts with the country code. Return
259 // null if the number is not a valid international number.
260 function ParseInternationalNumber(number) {
263 // Parse and strip the country code.
264 var countryCode = ParseCountryCode(number);
267 number = number.substr(countryCode.length);
269 // Lookup the meta data for the region (or regions) and if the rest of
270 // the number parses for that region, return the parsed number.
271 var entry = dataBase[countryCode];
272 if (Array.isArray(entry)) {
273 for (var n = 0; n < entry.length; ++n) {
274 if (typeof entry[n] == "string")
275 entry[n] = ParseMetaData(countryCode, entry[n]);
277 entry[n].formats = entry[0].formats;
278 ret = ParseNationalNumber(number, entry[n])
284 if (typeof entry == "string")
285 entry = dataBase[countryCode] = ParseMetaData(countryCode, entry);
286 return ParseNationalNumber(number, entry);
289 // Parse a national number for a specific region. Return null if the
290 // number is not a valid national number (it might still be a possible
291 // number for parts of that region).
292 function ParseNationalNumber(number, md) {
293 if (!md.possiblePattern.test(number) ||
294 !md.nationalPattern.test(number)) {
298 return new NationalNumber(md, number);
301 // Parse a number and transform it into the national format, removing any
302 // international dial prefixes and country codes.
303 function ParseNumber(number, defaultRegion) {
306 // Remove formating characters and whitespace.
307 number = PhoneNumberNormalizer.Normalize(number);
309 // If there is no defaultRegion or the defaultRegion is the global region,
310 // we can't parse international access codes.
311 if ((!defaultRegion || defaultRegion === '001') && number[0] !== '+')
314 // Detect and strip leading '+'.
315 if (number[0] === '+')
316 return ParseInternationalNumber(number.replace(LEADING_PLUS_CHARS_PATTERN, ""));
318 // Lookup the meta data for the given region.
319 var md = FindMetaDataForRegion(defaultRegion.toUpperCase());
322 dump("Couldn't find Meta Data for region: " + defaultRegion + "\n");
326 // See if the number starts with an international prefix, and if the
327 // number resulting from stripping the code is valid, then remove the
328 // prefix and flag the number as international.
329 if (md.internationalPrefix.test(number)) {
330 var possibleNumber = number.replace(md.internationalPrefix, "");
331 ret = ParseInternationalNumber(possibleNumber)
336 // This is not an international number. See if its a national one for
337 // the current region. National numbers can start with the national
338 // prefix, or without.
339 if (md.nationalPrefixForParsing) {
340 // Some regions have specific national prefix parse rules. Apply those.
341 var withoutPrefix = number.replace(md.nationalPrefixForParsing,
342 md.nationalPrefixTransformRule || '');
343 ret = ParseNationalNumber(withoutPrefix, md)
347 // If there is no specific national prefix rule, just strip off the
348 // national prefix from the beginning of the number (if there is one).
349 var nationalPrefix = md.nationalPrefix;
350 if (nationalPrefix && number.indexOf(nationalPrefix) == 0 &&
351 (ret = ParseNationalNumber(number.substr(nationalPrefix.length), md))) {
355 ret = ParseNationalNumber(number, md)
359 // Now lets see if maybe its an international number after all, but
360 // without '+' or the international prefix.
361 ret = ParseInternationalNumber(number)
365 // If the number matches the possible numbers of the current region,
366 // return it as a possible number.
367 if (md.possiblePattern.test(number))
368 return new NationalNumber(md, number);
370 // We couldn't parse the number at all.
374 function IsPlainPhoneNumber(number) {
375 if (typeof number !== 'string') {
379 var length = number.length;
380 var isTooLong = (length > MAX_PHONE_NUMBER_LENGTH);
381 var isEmpty = (length === 0);
382 return !(isTooLong || isEmpty || NON_DIALABLE_CHARS_ONCE.test(number));
386 IsPlain: IsPlainPhoneNumber,
389 })(PHONE_NUMBER_META_DATA);