@botique/libphonenumber-js
Version:
A simpler (and smaller) rewrite of Google Android's popular libphonenumber library
637 lines (543 loc) • 23.3 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.VALID_DIGITS = undefined;
var _extends2 = require('babel-runtime/helpers/extends');
var _extends3 = _interopRequireDefault(_extends2);
var _getIterator2 = require('babel-runtime/core-js/get-iterator');
var _getIterator3 = _interopRequireDefault(_getIterator2);
exports.default = parse;
exports.is_viable_phone_number = is_viable_phone_number;
exports.extract_formatted_phone_number = extract_formatted_phone_number;
exports.strip_national_prefix = strip_national_prefix;
exports.find_country_code = find_country_code;
var _common = require('./common');
var _metadata = require('./metadata');
var _metadata2 = _interopRequireDefault(_metadata);
var _types = require('./types');
var _types2 = _interopRequireDefault(_types);
var _RFC = require('./RFC3966');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// The minimum length of the national significant number.
// This is a port of Google Android `libphonenumber`'s
// `phonenumberutil.js` of 17th November, 2016.
//
// https://github.com/googlei18n/libphonenumber/commits/master/javascript/i18n/phonenumbers/phonenumberutil.js
var MIN_LENGTH_FOR_NSN = 2;
// The ITU says the maximum length should be 15,
// but one can find longer numbers in Germany.
var MAX_LENGTH_FOR_NSN = 17;
// We don't allow input strings for parsing to be longer than 250 chars.
// This prevents malicious input from consuming CPU.
var MAX_INPUT_STRING_LENGTH = 250;
// Digits accepted in phone numbers
// (ascii, fullwidth, arabic-indic, and eastern arabic digits).
var VALID_DIGITS = exports.VALID_DIGITS = '0-9\uFF10-\uFF19\u0660-\u0669\u06F0-\u06F9';
// Pattern to capture digits used in an extension.
// Places a maximum length of '7' for an extension.
var CAPTURING_EXTN_DIGITS = '([' + VALID_DIGITS + ']{1,7})';
// The RFC 3966 format for extensions.
var RFC3966_EXTN_PREFIX = ';ext=';
/**
* Regexp of all possible ways to write extensions, for use when parsing. This
* will be run as a case-insensitive regexp match. Wide character versions are
* also provided after each ASCII version. There are three regular expressions
* here. The first covers RFC 3966 format, where the extension is added using
* ';ext='. The second more generic one starts with optional white space and
* ends with an optional full stop (.), followed by zero or more spaces/tabs
* /commas and then the numbers themselves. The other one covers the special
* case of American numbers where the extension is written with a hash at the
* end, such as '- 503#'. Note that the only capturing groups should be around
* the digits that you want to capture as part of the extension, or else parsing
* will fail! We allow two options for representing the accented o - the
* character itself, and one in the unicode decomposed form with the combining
* acute accent.
*/
var EXTN_PATTERNS_FOR_PARSING = RFC3966_EXTN_PREFIX + CAPTURING_EXTN_DIGITS + '|' + '[ \xA0\\t,]*' + '(?:e?xt(?:ensi(?:o\u0301?|\xF3))?n?|\uFF45?\uFF58\uFF54\uFF4E?|' + '[;,x\uFF58#\uFF03~\uFF5E]|int|anexo|\uFF49\uFF4E\uFF54)' + '[:\\.\uFF0E]?[ \xA0\\t,-]*' + CAPTURING_EXTN_DIGITS + '#?|' + '[- ]+([' + VALID_DIGITS + ']{1,5})#';
// Regexp of all known extension prefixes used by different regions followed by
// 1 or more valid digits, for use when parsing.
var EXTN_PATTERN = new RegExp('(?:' + EXTN_PATTERNS_FOR_PARSING + ')$', 'i');
// Regular expression of viable phone numbers. This is location independent.
// Checks we have at least three leading digits, and only valid punctuation,
// alpha characters and digits in the phone number. Does not include extension
// data. The symbol 'x' is allowed here as valid punctuation since it is often
// used as a placeholder for carrier codes, for example in Brazilian phone
// numbers. We also allow multiple '+' characters at the start.
//
// Corresponds to the following:
// [digits]{minLengthNsn}|
// plus_sign*
// (([punctuation]|[star])*[digits]){3,}([punctuation]|[star]|[digits]|[alpha])*
//
// The first reg-ex is to allow short numbers (two digits long) to be parsed if
// they are entered as "15" etc, but only if there is no punctuation in them.
// The second expression restricts the number of digits to three or more, but
// then allows them to be in international form, and to have alpha-characters
// and punctuation. We split up the two reg-exes here and combine them when
// creating the reg-ex VALID_PHONE_NUMBER_PATTERN itself so we can prefix it
// with ^ and append $ to each branch.
//
// "Note VALID_PUNCTUATION starts with a -,
// so must be the first in the range" (c) Google devs.
// (wtf did they mean by saying that; probably nothing)
//
var MIN_LENGTH_PHONE_NUMBER_PATTERN = '[' + VALID_DIGITS + ']{' + MIN_LENGTH_FOR_NSN + '}';
//
// And this is the second reg-exp:
// (see MIN_LENGTH_PHONE_NUMBER_PATTERN for a full description of this reg-exp)
//
var VALID_PHONE_NUMBER = '[' + _common.PLUS_CHARS + ']{0,1}' + '(?:' + '[' + _common.VALID_PUNCTUATION + ']*' + '[' + VALID_DIGITS + ']' + '){3,}' + '[' + _common.VALID_PUNCTUATION + VALID_DIGITS + ']*';
// The combined regular expression for valid phone numbers:
//
var VALID_PHONE_NUMBER_PATTERN = new RegExp(
// Either a short two-digit-only phone number
'^' + MIN_LENGTH_PHONE_NUMBER_PATTERN + '$' + '|' +
// Or a longer fully parsed phone number (min 3 characters)
'^' + VALID_PHONE_NUMBER +
// Phone number extensions
'(?:' + EXTN_PATTERNS_FOR_PARSING + ')?' + '$', 'i');
// This consists of the plus symbol, digits, and arabic-indic digits.
var PHONE_NUMBER_START_PATTERN = new RegExp('[' + '\(' + _common.PLUS_CHARS + VALID_DIGITS + ']');
// Regular expression of trailing characters that we want to remove.
var AFTER_PHONE_NUMBER_END_PATTERN = new RegExp('[^' + VALID_DIGITS + ']+$');
var default_options = {
country: {}
// `options`:
// {
// country:
// {
// restrict - (a two-letter country code)
// the phone number must be in this country
//
// default - (a two-letter country code)
// default country to use for phone number parsing and validation
// (if no country code could be derived from the phone number)
// }
// }
//
// Returns `{ country, number }`
//
// Example use cases:
//
// ```js
// parse('8 (800) 555-35-35', 'RU')
// parse('8 (800) 555-35-35', 'RU', metadata)
// parse('8 (800) 555-35-35', { country: { default: 'RU' } })
// parse('8 (800) 555-35-35', { country: { default: 'RU' } }, metadata)
// parse('+7 800 555 35 35')
// parse('+7 800 555 35 35', metadata)
// ```
//
};function parse(arg_1, arg_2, arg_3, arg_4) {
var _sort_out_arguments = sort_out_arguments(arg_1, arg_2, arg_3, arg_4),
text = _sort_out_arguments.text,
options = _sort_out_arguments.options,
metadata = _sort_out_arguments.metadata;
// Validate `defaultCountry`.
if (options.defaultCountry && !metadata.hasCountry(options.defaultCountry)) {
throw new Error('Unknown country: ' + options.defaultCountry);
}
// Parse the phone number.
var _parse_input = parse_input(text),
formatted_phone_number = _parse_input.number,
ext = _parse_input.ext,
starts_at = _parse_input.starts_at,
ends_at = _parse_input.ends_at;
// If the phone number is not viable then return nothing.
if (!formatted_phone_number) {
return {};
}
var _parse_phone_number = parse_phone_number(formatted_phone_number, options.defaultCountry, metadata),
country = _parse_phone_number.country,
national_number = _parse_phone_number.national_number,
countryCallingCode = _parse_phone_number.countryCallingCode;
if (!metadata.selectedCountry()) {
return options.extended ? { countryCallingCode: countryCallingCode } : {};
}
// Validate national (significant) number length.
//
// A sidenote:
//
// They say that sometimes national (significant) numbers
// can be longer than `MAX_LENGTH_FOR_NSN` (e.g. in Germany).
// https://github.com/googlei18n/libphonenumber/blob/7e1748645552da39c4e1ba731e47969d97bdb539/resources/phonenumber.proto#L36
// Such numbers will just be discarded.
//
if (national_number.length < MIN_LENGTH_FOR_NSN || national_number.length > MAX_LENGTH_FOR_NSN) {
// Google's demo just throws an error in this case.
return {};
}
// Check if national phone number pattern matches the number
// National number pattern is different for each country,
// even for those ones which are part of the "NANPA" group.
var valid = country && (0, _common.matches_entirely)(national_number, new RegExp(metadata.nationalNumberPattern())) ? true : false;
if (!options.extended) {
return valid ? result(country, national_number, ext, starts_at, ends_at) : {};
}
return {
country: country,
countryCallingCode: countryCallingCode,
valid: valid,
possible: valid ? true : options.extended === true && metadata.possibleLengths() && is_possible_number(national_number, countryCallingCode !== undefined, metadata),
phone: national_number,
ext: ext,
starts_at: starts_at,
ends_at: ends_at
};
}
// Checks to see if the string of characters could possibly be a phone number at
// all. At the moment, checks to see that the string begins with at least 2
// digits, ignoring any punctuation commonly found in phone numbers. This method
// does not require the number to be normalized in advance - but does assume
// that leading non-number symbols have been removed, such as by the method
// `extract_possible_number`.
//
function is_viable_phone_number(number) {
return number.length >= MIN_LENGTH_FOR_NSN && (0, _common.matches_entirely)(number, VALID_PHONE_NUMBER_PATTERN);
}
function extract_formatted_phone_number(text) {
if (!text || text.length > MAX_INPUT_STRING_LENGTH) {
return {
number: '',
starts_at: -1,
ends_at: -1
};
}
// Attempt to extract a possible number from the string passed in
var starts_at = text.search(PHONE_NUMBER_START_PATTERN);
var ends_at_index = text.search(AFTER_PHONE_NUMBER_END_PATTERN);
var ends_at = ends_at_index > -1 ? ends_at_index : text.length;
if (starts_at < 0) {
return { number: '', starts_at: -1, ends_at: -1 };
}
// Return the number, the first index the number appeared in the text,
// and the index AFTER the last appearance
return {
number: text.slice(starts_at, ends_at),
starts_at: starts_at,
ends_at: ends_at
};
}
// Strips any national prefix (such as 0, 1) present in the number provided
function strip_national_prefix(number, metadata) {
if (!number || !metadata.nationalPrefixForParsing()) {
return number;
}
// Attempt to parse the first digits as a national prefix
var national_prefix_pattern = new RegExp('^(?:' + metadata.nationalPrefixForParsing() + ')');
var national_prefix_matcher = national_prefix_pattern.exec(number);
// If no national prefix is present in the phone number,
// but if the national prefix is optional for this country,
// then consider this phone number valid.
//
// Google's reference `libphonenumber` implementation
// wouldn't recognize such phone numbers as valid,
// but I think it would perfectly make sense
// to consider such phone numbers as valid
// because if a national phone number was originally
// formatted without the national prefix
// then it must be parseable back into the original national number.
// In other words, `parse(format(number))`
// must always be equal to `number`.
//
if (!national_prefix_matcher) {
return number;
}
var national_significant_number = void 0;
// `national_prefix_for_parsing` capturing groups
// (used only for really messy cases: Argentina, Brazil, Mexico, Somalia)
var any_groups_were_captured = national_prefix_matcher[national_prefix_matcher.length - 1];
// If the national number tranformation is needed then do it
if (metadata.nationalPrefixTransformRule() && any_groups_were_captured) {
national_significant_number = number.replace(national_prefix_pattern, metadata.nationalPrefixTransformRule());
}
// Else, no transformation is necessary,
// and just strip the national prefix.
else {
national_significant_number = number.slice(national_prefix_matcher[0].length);
}
// // Verify the parsed national (significant) number for this country
// const national_number_rule = new RegExp(metadata.nationalNumberPattern())
// //
// // If the original number (before stripping national prefix) was viable,
// // and the resultant number is not, then prefer the original phone number.
// // This is because for some countries (e.g. Russia) the same digit could be both
// // a national prefix and a leading digit of a valid national phone number,
// // like `8` is the national prefix for Russia and both
// // `8 800 555 35 35` and `800 555 35 35` are valid numbers.
// if (matches_entirely(number, national_number_rule) &&
// !matches_entirely(national_significant_number, national_number_rule))
// {
// return number
// }
// Return the parsed national (significant) number
return national_significant_number;
}
function find_country_code(country_calling_code, national_phone_number, metadata) {
// Is always non-empty, because `country_calling_code` is always valid
var possible_countries = metadata.countryCallingCodes()[country_calling_code];
// If there's just one country corresponding to the country code,
// then just return it, without further phone number digits validation.
if (possible_countries.length === 1) {
return possible_countries[0];
}
var original_country = metadata.selectedCountry();
var country = _find_country_code(possible_countries, national_phone_number, metadata);
metadata.country(original_country);
return country;
}
// Changes `metadata` `country`.
function _find_country_code(possible_countries, national_phone_number, metadata) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = (0, _getIterator3.default)(possible_countries), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var country = _step.value;
metadata.country(country);
// Leading digits check would be the simplest one
if (metadata.leadingDigits()) {
if (national_phone_number && national_phone_number.search(metadata.leadingDigits()) === 0) {
return country;
}
}
// Else perform full validation with all of those
// fixed-line/mobile/etc regular expressions.
else if ((0, _types2.default)({ phone: national_phone_number, country: country }, metadata.metadata)) {
return country;
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
}
// Sort out arguments
function sort_out_arguments(arg_1, arg_2, arg_3, arg_4) {
var text = void 0;
var options = void 0;
var metadata = void 0;
// If the phone number is passed as a string.
// `parse('88005553535', ...)`.
if (typeof arg_1 === 'string') {
text = arg_1;
} else throw new TypeError('A phone number for parsing must be a string.');
// If "default country" argument is being passed
// then move it to `options`.
// `parse('88005553535', 'RU', [options], metadata)`.
if (typeof arg_2 === 'string') {
if (arg_4) {
options = (0, _extends3.default)({ defaultCountry: arg_2 }, arg_3);
metadata = arg_4;
} else {
options = { defaultCountry: arg_2 };
metadata = arg_3;
}
}
// No "resrict country" argument is being passed.
// International phone number is passed.
// `parse('+78005553535', [options], metadata)`.
else {
if (arg_3) {
options = arg_2;
metadata = arg_3;
} else {
metadata = arg_2;
}
}
// Metadata is required.
if (!metadata || !metadata.countries) {
throw new Error('Metadata is required');
}
// Apply default options.
if (options) {
options = (0, _extends3.default)({}, default_options, options);
} else {
options = default_options;
}
return { text: text, options: options, metadata: new _metadata2.default(metadata) };
}
// Strips any extension (as in, the part of the number dialled after the call is
// connected, usually indicated with extn, ext, x or similar) from the end of
// the number, and returns it.
function strip_extension(number) {
var start = number.search(EXTN_PATTERN);
if (start < 0) {
return {};
}
// If we find a potential extension, and the number preceding this is a viable
// number, we assume it is an extension.
var number_without_extension = number.slice(0, start);
/* istanbul ignore if - seems a bit of a redundant check */
if (!is_viable_phone_number(number_without_extension)) {
return {};
}
var matches = number.match(EXTN_PATTERN);
var i = 1;
while (i < matches.length) {
if (matches[i] != null && matches[i].length > 0) {
return {
number: number_without_extension,
ext: matches[i]
};
}
i++;
}
}
function is_possible_number(national_number, is_international, metadata) {
switch ((0, _types.check_number_length_for_type)(national_number, undefined, metadata)) {
case 'IS_POSSIBLE':
return true;
// case 'IS_POSSIBLE_LOCAL_ONLY':
// return !is_international
default:
return false;
}
}
/**
* Parses a viable international phone number.
* Returns `{ country, national_number }`.
*/
function get_country_and_national_number_international(country_calling_code, national_number, metadata) {
// Sometimes there are several countries
// corresponding to the same country phone code
// (e.g. NANPA countries all having `1` country phone code).
// Therefore, to reliably determine the exact country,
// national (significant) number should have been parsed first.
//
// When `metadata.json` is generated, all "ambiguous" country phone codes
// get their countries populated with the full set of
// "phone number type" regular expressions.
//
var country = find_country_code(country_calling_code, national_number, metadata);
// Formatting information for regions which share
// a country calling code is contained by only one region
// for performance reasons. For example, for NANPA region
// ("North American Numbering Plan Administration",
// which includes USA, Canada, Cayman Islands, Bahamas, etc)
// it will be contained in the metadata for `US`.
country ? metadata.country(country) : metadata.chooseCountryByCountryCallingCode(country_calling_code);
return { national_number: national_number, country: country };
}
/**
* Parses a viable local phone number.
* Returns `{ country, national_number }`.
*/
function get_country_and_national_number_local(formatted_phone_number, default_country, metadata) {
var country = default_country;
metadata.country(country);
var national_number = (0, _common.parse_phone_number_digits)(formatted_phone_number);
// Only strip national prefixes for non-international phone numbers
// because national prefixes can't be present in international phone numbers.
// Otherwise, while forgiving, it would parse a NANPA number `+1 1877 215 5230`
// first to `1877 215 5230` and then, stripping the leading `1`, to `877 215 5230`,
// and then it would assume that's a valid number which it isn't.
// So no forgiveness for grandmas here.
// The issue asking for this fix:
// https://github.com/catamphetamine/libphonenumber-js/issues/159
var potential_national_number = strip_national_prefix(national_number, metadata);
// If metadata has "possible lengths" then employ the new algorythm.
if (metadata.possibleLengths()) {
// We require that the NSN remaining after stripping the national prefix and
// carrier code be long enough to be a possible length for the region.
// Otherwise, we don't do the stripping, since the original number could be
// a valid short number.
switch ((0, _types.check_number_length_for_type)(potential_national_number, undefined, metadata)) {
case 'TOO_SHORT':
// case 'IS_POSSIBLE_LOCAL_ONLY':
case 'INVALID_LENGTH':
break;
default:
national_number = potential_national_number;
}
} else {
// If the original number (before stripping national prefix) was viable,
// and the resultant number is not, then prefer the original phone number.
// This is because for some countries (e.g. Russia) the same digit could be both
// a national prefix and a leading digit of a valid national phone number,
// like `8` is the national prefix for Russia and both
// `8 800 555 35 35` and `800 555 35 35` are valid numbers.
if ((0, _common.matches_entirely)(national_number, metadata.nationalNumberPattern()) && !(0, _common.matches_entirely)(potential_national_number, metadata.nationalNumberPattern())) {
// Keep the number without stripping national prefix.
} else {
national_number = potential_national_number;
}
}
return { national_number: national_number, country: country };
}
/**
* @param {string} text - Input.
* @return {object} `{ ?number, ?ext }`.
*/
function parse_input(text) {
// Parse RFC 3966 phone number URI.
if (text && text.indexOf('tel:') === 0) {
return (0, _RFC.parseRFC3966)(text);
}
var _extract_formatted_ph = extract_formatted_phone_number(text),
number = _extract_formatted_ph.number,
starts_at = _extract_formatted_ph.starts_at,
ends_at = _extract_formatted_ph.ends_at;
// If the phone number is not viable, then abort.
if (!is_viable_phone_number(number)) {
return {};
}
// Attempt to parse extension first, since it doesn't require region-specific
// data and we want to have the non-normalised number here.
var with_extension_stripped = strip_extension(number);
if (with_extension_stripped.ext) {
return (0, _extends3.default)({}, with_extension_stripped, {
starts_at: starts_at,
ends_at: ends_at
});
}
return { number: number, starts_at: starts_at, ends_at: ends_at };
}
function result(country, national_number, ext, starts_at, ends_at) {
var result = {
country: country,
phone: national_number,
starts_at: starts_at,
ends_at: ends_at
};
if (ext) {
result.ext = ext;
}
return result;
}
/**
* Parses a viable phone number.
* Returns `{ country, countryCallingCode, national_number }`.
*/
function parse_phone_number(formatted_phone_number, default_country, metadata) {
var _parse_national_numbe = (0, _common.parse_national_number_and_country_calling_code)(formatted_phone_number, metadata),
countryCallingCode = _parse_national_numbe.countryCallingCode,
number = _parse_national_numbe.number;
if (!number) {
return { countryCallingCode: countryCallingCode };
}
if (countryCallingCode) {
var _result = get_country_and_national_number_international(countryCallingCode, number, metadata);
_result.countryCallingCode = countryCallingCode;
return _result;
}
if (default_country) {
var _result2 = get_country_and_national_number_local(formatted_phone_number, default_country, metadata);
metadata.country(default_country);
_result2.countryCallingCode = metadata.countryCallingCode();
return _result2;
}
return {};
}
//# sourceMappingURL=parse.js.map