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);