|
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ |
|
3 |
|
4 // Don't modify this code. Please use: |
|
5 // https://github.com/andreasgal/PhoneNumber.js |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 this.EXPORTED_SYMBOLS = ["PhoneNumber"]; |
|
10 |
|
11 Components.utils.import("resource://gre/modules/PhoneNumberMetaData.jsm"); |
|
12 Components.utils.import("resource://gre/modules/PhoneNumberNormalizer.jsm"); |
|
13 |
|
14 this.PhoneNumber = (function (dataBase) { |
|
15 // Use strict in our context only - users might not want it |
|
16 'use strict'; |
|
17 |
|
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; |
|
25 |
|
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"]; |
|
38 |
|
39 const FORMAT_ENCODING = ["^pattern$", |
|
40 "nationalFormat", |
|
41 "^leadingDigits", |
|
42 "nationalPrefixFormattingRule", |
|
43 "internationalFormat"]; |
|
44 |
|
45 var regionCache = Object.create(null); |
|
46 |
|
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 } |
|
62 |
|
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 } |
|
73 |
|
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 } |
|
90 |
|
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 } |
|
124 |
|
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 } |
|
135 |
|
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 } |
|
194 |
|
195 function NationalNumber(regionMetaData, number) { |
|
196 this.region = regionMetaData.region; |
|
197 this.regionMetaData = regionMetaData; |
|
198 this.nationalNumber = number; |
|
199 } |
|
200 |
|
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 }; |
|
232 |
|
233 // Check whether the number is valid for the given region. |
|
234 function IsValidNumber(number, md) { |
|
235 return md.possiblePattern.test(number); |
|
236 } |
|
237 |
|
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 } |
|
242 |
|
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 } |
|
253 |
|
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; |
|
258 |
|
259 // Parse and strip the country code. |
|
260 var countryCode = ParseCountryCode(number); |
|
261 if (!countryCode) |
|
262 return null; |
|
263 number = number.substr(countryCode.length); |
|
264 |
|
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 } |
|
284 |
|
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 } |
|
296 |
|
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; |
|
301 |
|
302 // Remove formating characters and whitespace. |
|
303 number = PhoneNumberNormalizer.Normalize(number); |
|
304 |
|
305 // If there is no defaultRegion, we can't parse international access codes. |
|
306 if (!defaultRegion && number[0] !== '+') |
|
307 return null; |
|
308 |
|
309 // Detect and strip leading '+'. |
|
310 if (number[0] === '+') |
|
311 return ParseInternationalNumber(number.replace(LEADING_PLUS_CHARS_PATTERN, "")); |
|
312 |
|
313 // Lookup the meta data for the given region. |
|
314 var md = FindMetaDataForRegion(defaultRegion.toUpperCase()); |
|
315 |
|
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 } |
|
325 |
|
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; |
|
348 |
|
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; |
|
354 |
|
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); |
|
359 |
|
360 // We couldn't parse the number at all. |
|
361 return null; |
|
362 } |
|
363 |
|
364 function IsPlainPhoneNumber(number) { |
|
365 if (typeof number !== 'string') { |
|
366 return false; |
|
367 } |
|
368 |
|
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 } |
|
374 |
|
375 return { |
|
376 IsPlain: IsPlainPhoneNumber, |
|
377 Parse: ParseNumber, |
|
378 }; |
|
379 })(PHONE_NUMBER_META_DATA); |