🎯 MapView v2.0 - Global Deployment Ready

 MAJOR FEATURES:
• Auto-zoom intelligence với smart bounds fitting
• Enhanced 3D GPS markers với pulsing effects
• Professional route display với 6-layer rendering
• Status-based parking icons với availability indicators
• Production-ready build optimizations

🗺️ AUTO-ZOOM FEATURES:
• Smart bounds fitting cho GPS + selected parking
• Adaptive padding (50px) cho visual balance
• Max zoom control (level 16) để tránh quá gần
• Dynamic centering khi không có selection

🎨 ENHANCED VISUALS:
• 3D GPS marker với multi-layer pulse effects
• Advanced parking icons với status colors
• Selection highlighting với animation
• Dimming system cho non-selected items

🛣️ ROUTE SYSTEM:
• OpenRouteService API integration
• Multi-layer route rendering (glow, shadow, main, animated)
• Real-time distance & duration calculation
• Visual route info trong popup

📱 PRODUCTION READY:
• SSR safe với dynamic imports
• Build errors resolved
• Global deployment via Vercel
• Optimized performance

🌍 DEPLOYMENT:
• Vercel: https://whatever-ctk2auuxr-phong12hexdockworks-projects.vercel.app
• Bundle size: 22.8 kB optimized
• Global CDN distribution
• HTTPS enabled

💾 VERSION CONTROL:
• MapView-v2.0.tsx backup created
• MAPVIEW_VERSIONS.md documentation
• Full version history tracking
This commit is contained in:
2025-07-20 19:52:16 +07:00
parent 3203463a6a
commit c65cc97a33
64624 changed files with 7199453 additions and 6462 deletions

View File

@@ -0,0 +1,59 @@
import isViablePhoneNumber from './isViablePhoneNumber.js'
// https://www.ietf.org/rfc/rfc3966.txt
/**
* @param {string} text - Phone URI (RFC 3966).
* @return {object} `{ ?number, ?ext }`.
*/
export function parseRFC3966(text) {
let number
let ext
// Replace "tel:" with "tel=" for parsing convenience.
text = text.replace(/^tel:/, 'tel=')
for (const part of text.split(';')) {
const [name, value] = part.split('=')
switch (name) {
case 'tel':
number = value
break
case 'ext':
ext = value
break
case 'phone-context':
// Only "country contexts" are supported.
// "Domain contexts" are ignored.
if (value[0] === '+') {
number = value + number
}
break
}
}
// If the phone number is not viable, then abort.
if (!isViablePhoneNumber(number)) {
return {}
}
const result = { number }
if (ext) {
result.ext = ext
}
return result
}
/**
* @param {object} - `{ ?number, ?extension }`.
* @return {string} Phone URI (RFC 3966).
*/
export function formatRFC3966({ number, ext }) {
if (!number) {
return ''
}
if (number[0] !== '+') {
throw new Error(`"formatRFC3966()" expects "number" to be in E.164 format.`)
}
return `tel:${number}${ext ? ';ext=' + ext : ''}`
}

View File

@@ -0,0 +1,36 @@
import { parseRFC3966, formatRFC3966 } from './RFC3966.js'
describe('RFC3966', () => {
it('should format', () => {
expect(() => formatRFC3966({ number: '123' })).to.throw('expects "number" to be in E.164 format')
formatRFC3966({}).should.equal('')
formatRFC3966({ number: '+78005553535' }).should.equal('tel:+78005553535')
formatRFC3966({ number: '+78005553535', ext: '123' }).should.equal('tel:+78005553535;ext=123')
})
it('should parse', () => {
parseRFC3966('tel:+78005553535').should.deep.equal({
number : '+78005553535'
})
parseRFC3966('tel:+78005553535;ext=123').should.deep.equal({
number : '+78005553535',
ext : '123'
})
// With `phone-context`
parseRFC3966('tel:8005553535;ext=123;phone-context=+7').should.deep.equal({
number : '+78005553535',
ext : '123'
})
// "Domain contexts" are ignored
parseRFC3966('tel:8005553535;ext=123;phone-context=www.leningrad.spb.ru').should.deep.equal({
number : '8005553535',
ext : '123'
})
// Not a viable phone number.
parseRFC3966('tel:3').should.deep.equal({})
})
})

View File

@@ -0,0 +1,35 @@
import { VALID_PUNCTUATION } from '../constants.js'
// Removes brackets and replaces dashes with spaces.
//
// E.g. "(999) 111-22-33" -> "999 111 22 33"
//
// For some reason Google's metadata contains `<intlFormat/>`s with brackets and dashes.
// Meanwhile, there's no single opinion about using punctuation in international phone numbers.
//
// For example, Google's `<intlFormat/>` for USA is `+1 213-373-4253`.
// And here's a quote from WikiPedia's "North American Numbering Plan" page:
// https://en.wikipedia.org/wiki/North_American_Numbering_Plan
//
// "The country calling code for all countries participating in the NANP is 1.
// In international format, an NANP number should be listed as +1 301 555 01 00,
// where 301 is an area code (Maryland)."
//
// I personally prefer the international format without any punctuation.
// For example, brackets are remnants of the old age, meaning that the
// phone number part in brackets (so called "area code") can be omitted
// if dialing within the same "area".
// And hyphens were clearly introduced for splitting local numbers into memorizable groups.
// For example, remembering "5553535" is difficult but "555-35-35" is much simpler.
// Imagine a man taking a bus from home to work and seeing an ad with a phone number.
// He has a couple of seconds to memorize that number until it passes by.
// If it were spaces instead of hyphens the man wouldn't necessarily get it,
// but with hyphens instead of spaces the grouping is more explicit.
// I personally think that hyphens introduce visual clutter,
// so I prefer replacing them with spaces in international numbers.
// In the modern age all output is done on displays where spaces are clearly distinguishable
// so hyphens can be safely replaced with spaces without losing any legibility.
//
export default function applyInternationalSeparatorStyle(formattedNumber) {
return formattedNumber.replace(new RegExp(`[${VALID_PUNCTUATION}]+`, 'g'), ' ').trim()
}

View File

@@ -0,0 +1,8 @@
import applyInternationalSeparatorStyle from './applyInternationalSeparatorStyle.js'
describe('applyInternationalSeparatorStyle', () => {
it('should change Google\'s international format style', () => {
applyInternationalSeparatorStyle('(xxx) xxx-xx-xx').should.equal('xxx xxx xx xx')
applyInternationalSeparatorStyle('(xxx)xxx').should.equal('xxx xxx')
})
})

View File

@@ -0,0 +1,86 @@
import mergeArrays from './mergeArrays.js'
export default function checkNumberLength(nationalNumber, metadata) {
return checkNumberLengthForType(nationalNumber, undefined, metadata)
}
// Checks whether a number is possible for the country based on its length.
// Should only be called for the "new" metadata which has "possible lengths".
export function checkNumberLengthForType(nationalNumber, type, metadata) {
const type_info = metadata.type(type)
// There should always be "<possiblePengths/>" set for every type element.
// This is declared in the XML schema.
// For size efficiency, where a sub-description (e.g. fixed-line)
// has the same "<possiblePengths/>" as the "general description", this is missing,
// so we fall back to the "general description". Where no numbers of the type
// exist at all, there is one possible length (-1) which is guaranteed
// not to match the length of any real phone number.
let possible_lengths = type_info && type_info.possibleLengths() || metadata.possibleLengths()
// let local_lengths = type_info && type.possibleLengthsLocal() || metadata.possibleLengthsLocal()
// Metadata before version `1.0.18` didn't contain `possible_lengths`.
if (!possible_lengths) {
return 'IS_POSSIBLE'
}
if (type === 'FIXED_LINE_OR_MOBILE') {
// No such country in metadata.
/* istanbul ignore next */
if (!metadata.type('FIXED_LINE')) {
// The rare case has been encountered where no fixedLine data is available
// (true for some non-geographic entities), so we just check mobile.
return checkNumberLengthForType(nationalNumber, 'MOBILE', metadata)
}
const mobile_type = metadata.type('MOBILE')
if (mobile_type) {
// Merge the mobile data in if there was any. "Concat" creates a new
// array, it doesn't edit possible_lengths in place, so we don't need a copy.
// Note that when adding the possible lengths from mobile, we have
// to again check they aren't empty since if they are this indicates
// they are the same as the general desc and should be obtained from there.
possible_lengths = mergeArrays(possible_lengths, mobile_type.possibleLengths())
// The current list is sorted; we need to merge in the new list and
// re-sort (duplicates are okay). Sorting isn't so expensive because
// the lists are very small.
// if (local_lengths) {
// local_lengths = mergeArrays(local_lengths, mobile_type.possibleLengthsLocal())
// } else {
// local_lengths = mobile_type.possibleLengthsLocal()
// }
}
}
// If the type doesn't exist then return 'INVALID_LENGTH'.
else if (type && !type_info) {
return 'INVALID_LENGTH'
}
const actual_length = nationalNumber.length
// In `libphonenumber-js` all "local-only" formats are dropped for simplicity.
// // This is safe because there is never an overlap beween the possible lengths
// // and the local-only lengths; this is checked at build time.
// if (local_lengths && local_lengths.indexOf(nationalNumber.length) >= 0)
// {
// return 'IS_POSSIBLE_LOCAL_ONLY'
// }
const minimum_length = possible_lengths[0]
if (minimum_length === actual_length) {
return 'IS_POSSIBLE'
}
if (minimum_length > actual_length) {
return 'TOO_SHORT'
}
if (possible_lengths[possible_lengths.length - 1] < actual_length) {
return 'TOO_LONG'
}
// We skip the first element since we've already checked it.
return possible_lengths.indexOf(actual_length, 1) >= 0 ? 'IS_POSSIBLE' : 'INVALID_LENGTH'
}

View File

@@ -0,0 +1,40 @@
import Metadata from '../metadata.js'
import metadata from '../../metadata.max.json' assert { type: 'json' }
import oldMetadata from '../../test/metadata/1.0.0/metadata.min.json' assert { type: 'json' }
import { checkNumberLengthForType } from './checkNumberLength.js'
describe('checkNumberLength', () => {
it('should check phone number length', () => {
// Too short.
checkNumberLength('800555353', 'FIXED_LINE', 'RU').should.equal('TOO_SHORT')
// Normal.
checkNumberLength('8005553535', 'FIXED_LINE', 'RU').should.equal('IS_POSSIBLE')
// Too long.
checkNumberLength('80055535355', 'FIXED_LINE', 'RU').should.equal('TOO_LONG')
// No such type.
checkNumberLength('169454850', 'VOIP', 'AC').should.equal('INVALID_LENGTH')
// No such possible length.
checkNumberLength('1694548', undefined, 'AD').should.equal('INVALID_LENGTH')
// FIXED_LINE_OR_MOBILE
checkNumberLength('1694548', 'FIXED_LINE_OR_MOBILE', 'AD').should.equal('INVALID_LENGTH')
// No mobile phones.
checkNumberLength('8123', 'FIXED_LINE_OR_MOBILE', 'TA').should.equal('IS_POSSIBLE')
// No "possible lengths" for "mobile".
checkNumberLength('81234567', 'FIXED_LINE_OR_MOBILE', 'SZ').should.equal('IS_POSSIBLE')
})
it('should work for old metadata', function() {
const _oldMetadata = new Metadata(oldMetadata)
_oldMetadata.country('RU')
checkNumberLengthForType('8005553535', 'FIXED_LINE', _oldMetadata).should.equal('IS_POSSIBLE')
})
})
function checkNumberLength(number, type, country) {
const _metadata = new Metadata(metadata)
_metadata.country(country)
return checkNumberLengthForType(number, type, _metadata)
}

View File

@@ -0,0 +1,112 @@
import { VALID_DIGITS } from '../../constants.js'
// The RFC 3966 format for extensions.
const RFC3966_EXTN_PREFIX = ';ext='
/**
* Helper method for constructing regular expressions for parsing. Creates
* an expression that captures up to max_length digits.
* @return {string} RegEx pattern to capture extension digits.
*/
const getExtensionDigitsPattern = (maxLength) => `([${VALID_DIGITS}]{1,${maxLength}})`
/**
* Helper initialiser method to create the regular-expression pattern to match
* extensions.
* Copy-pasted from Google's `libphonenumber`:
* https://github.com/google/libphonenumber/blob/55b2646ec9393f4d3d6661b9c82ef9e258e8b829/javascript/i18n/phonenumbers/phonenumberutil.js#L759-L766
* @return {string} RegEx pattern to capture extensions.
*/
export default function createExtensionPattern(purpose) {
// We cap the maximum length of an extension based on the ambiguity of the way
// the extension is prefixed. As per ITU, the officially allowed length for
// extensions is actually 40, but we don't support this since we haven't seen real
// examples and this introduces many false interpretations as the extension labels
// are not standardized.
/** @type {string} */
var extLimitAfterExplicitLabel = '20';
/** @type {string} */
var extLimitAfterLikelyLabel = '15';
/** @type {string} */
var extLimitAfterAmbiguousChar = '9';
/** @type {string} */
var extLimitWhenNotSure = '6';
/** @type {string} */
var possibleSeparatorsBetweenNumberAndExtLabel = "[ \u00A0\\t,]*";
// Optional full stop (.) or colon, followed by zero or more spaces/tabs/commas.
/** @type {string} */
var possibleCharsAfterExtLabel = "[:\\.\uFF0E]?[ \u00A0\\t,-]*";
/** @type {string} */
var optionalExtnSuffix = "#?";
// Here the extension is called out in more explicit way, i.e mentioning it obvious
// patterns like "ext.".
/** @type {string} */
var explicitExtLabels =
"(?:e?xt(?:ensi(?:o\u0301?|\u00F3))?n?|\uFF45?\uFF58\uFF54\uFF4E?|\u0434\u043E\u0431|anexo)";
// One-character symbols that can be used to indicate an extension, and less
// commonly used or more ambiguous extension labels.
/** @type {string} */
var ambiguousExtLabels = "(?:[x\uFF58#\uFF03~\uFF5E]|int|\uFF49\uFF4E\uFF54)";
// When extension is not separated clearly.
/** @type {string} */
var ambiguousSeparator = "[- ]+";
// This is the same as possibleSeparatorsBetweenNumberAndExtLabel, but not matching
// comma as extension label may have it.
/** @type {string} */
var possibleSeparatorsNumberExtLabelNoComma = "[ \u00A0\\t]*";
// ",," is commonly used for auto dialling the extension when connected. First
// comma is matched through possibleSeparatorsBetweenNumberAndExtLabel, so we do
// not repeat it here. Semi-colon works in Iphone and Android also to pop up a
// button with the extension number following.
/** @type {string} */
var autoDiallingAndExtLabelsFound = "(?:,{2}|;)";
/** @type {string} */
var rfcExtn = RFC3966_EXTN_PREFIX
+ getExtensionDigitsPattern(extLimitAfterExplicitLabel);
/** @type {string} */
var explicitExtn = possibleSeparatorsBetweenNumberAndExtLabel + explicitExtLabels
+ possibleCharsAfterExtLabel
+ getExtensionDigitsPattern(extLimitAfterExplicitLabel)
+ optionalExtnSuffix;
/** @type {string} */
var ambiguousExtn = possibleSeparatorsBetweenNumberAndExtLabel + ambiguousExtLabels
+ possibleCharsAfterExtLabel
+ getExtensionDigitsPattern(extLimitAfterAmbiguousChar)
+ optionalExtnSuffix;
/** @type {string} */
var americanStyleExtnWithSuffix = ambiguousSeparator
+ getExtensionDigitsPattern(extLimitWhenNotSure) + "#";
/** @type {string} */
var autoDiallingExtn = possibleSeparatorsNumberExtLabelNoComma
+ autoDiallingAndExtLabelsFound + possibleCharsAfterExtLabel
+ getExtensionDigitsPattern(extLimitAfterLikelyLabel)
+ optionalExtnSuffix;
/** @type {string} */
var onlyCommasExtn = possibleSeparatorsNumberExtLabelNoComma
+ "(?:,)+" + possibleCharsAfterExtLabel
+ getExtensionDigitsPattern(extLimitAfterAmbiguousChar)
+ optionalExtnSuffix;
// The first regular expression covers RFC 3966 format, where the extension is added
// using ";ext=". The second more generic where extension is mentioned with explicit
// labels like "ext:". In both the above cases we allow more numbers in extension than
// any other extension labels. The third one captures when single character extension
// labels or less commonly used labels are used. In such cases we capture fewer
// extension digits in order to reduce the chance of falsely interpreting two
// numbers beside each other as a number + extension. The fourth one covers the
// special case of American numbers where the extension is written with a hash
// at the end, such as "- 503#". The fifth one is exclusively for extension
// autodialling formats which are used when dialling and in this case we accept longer
// extensions. The last one is more liberal on the number of commas that acts as
// extension labels, so we have a strict cap on the number of digits in such extensions.
return rfcExtn + "|"
+ explicitExtn + "|"
+ ambiguousExtn + "|"
+ americanStyleExtnWithSuffix + "|"
+ autoDiallingExtn + "|"
+ onlyCommasExtn;
}

View File

@@ -0,0 +1,29 @@
import createExtensionPattern from './createExtensionPattern.js'
// Regexp of all known extension prefixes used by different regions followed by
// 1 or more valid digits, for use when parsing.
const EXTN_PATTERN = new RegExp('(?:' + createExtensionPattern() + ')$', 'i')
// 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.
export default function extractExtension(number) {
const 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.
const numberWithoutExtension = number.slice(0, start)
const matches = number.match(EXTN_PATTERN)
let i = 1
while (i < matches.length) {
if (matches[i]) {
return {
number: numberWithoutExtension,
ext: matches[i]
}
}
i++
}
}

View File

@@ -0,0 +1,151 @@
import stripIddPrefix from './stripIddPrefix.js'
import extractCountryCallingCodeFromInternationalNumberWithoutPlusSign from './extractCountryCallingCodeFromInternationalNumberWithoutPlusSign.js'
import Metadata from '../metadata.js'
import { MAX_LENGTH_COUNTRY_CODE } from '../constants.js'
/**
* Converts a phone number digits (possibly with a `+`)
* into a calling code and the rest phone number digits.
* The "rest phone number digits" could include
* a national prefix, carrier code, and national
* (significant) number.
* @param {string} number — Phone number digits (possibly with a `+`).
* @param {string} [country] — Default country.
* @param {string} [callingCode] — Default calling code (some phone numbering plans are non-geographic).
* @param {object} metadata
* @return {object} `{ countryCallingCodeSource: string?, countryCallingCode: string?, number: string }`
* @example
* // Returns `{ countryCallingCode: "1", number: "2133734253" }`.
* extractCountryCallingCode('2133734253', 'US', null, metadata)
* extractCountryCallingCode('2133734253', null, '1', metadata)
* extractCountryCallingCode('+12133734253', null, null, metadata)
* extractCountryCallingCode('+12133734253', 'RU', null, metadata)
*/
export default function extractCountryCallingCode(
number,
country,
callingCode,
metadata
) {
if (!number) {
return {}
}
let isNumberWithIddPrefix
// If this is not an international phone number,
// then either extract an "IDD" prefix, or extract a
// country calling code from a number by autocorrecting it
// by prepending a leading `+` in cases when it starts
// with the country calling code.
// https://wikitravel.org/en/International_dialling_prefix
// https://github.com/catamphetamine/libphonenumber-js/issues/376
if (number[0] !== '+') {
// Convert an "out-of-country" dialing phone number
// to a proper international phone number.
const numberWithoutIDD = stripIddPrefix(number, country, callingCode, metadata)
// If an IDD prefix was stripped then
// convert the number to international one
// for subsequent parsing.
if (numberWithoutIDD && numberWithoutIDD !== number) {
isNumberWithIddPrefix = true
number = '+' + numberWithoutIDD
} else {
// Check to see if the number starts with the country calling code
// for the default country. If so, we remove the country calling code,
// and do some checks on the validity of the number before and after.
// https://github.com/catamphetamine/libphonenumber-js/issues/376
if (country || callingCode) {
const {
countryCallingCode,
number: shorterNumber
} = extractCountryCallingCodeFromInternationalNumberWithoutPlusSign(
number,
country,
callingCode,
metadata
)
if (countryCallingCode) {
return {
countryCallingCodeSource: 'FROM_NUMBER_WITHOUT_PLUS_SIGN',
countryCallingCode,
number: shorterNumber
}
}
}
return {
// No need to set it to `UNSPECIFIED`. It can be just `undefined`.
// countryCallingCodeSource: 'UNSPECIFIED',
number
}
}
}
// Fast abortion: country codes do not begin with a '0'
if (number[1] === '0') {
return {}
}
metadata = new Metadata(metadata)
// The thing with country phone codes
// is that they are orthogonal to each other
// i.e. there's no such country phone code A
// for which country phone code B exists
// where B starts with A.
// Therefore, while scanning digits,
// if a valid country code is found,
// that means that it is the country code.
//
let i = 2
while (i - 1 <= MAX_LENGTH_COUNTRY_CODE && i <= number.length) {
const countryCallingCode = number.slice(1, i)
if (metadata.hasCallingCode(countryCallingCode)) {
metadata.selectNumberingPlan(countryCallingCode)
return {
countryCallingCodeSource: isNumberWithIddPrefix ? 'FROM_NUMBER_WITH_IDD' : 'FROM_NUMBER_WITH_PLUS_SIGN',
countryCallingCode,
number: number.slice(i)
}
}
i++
}
return {}
}
// The possible values for the returned `countryCallingCodeSource` are:
//
// Copy-pasted from:
// https://github.com/google/libphonenumber/blob/master/resources/phonenumber.proto
//
// // The source from which the country_code is derived. This is not set in the
// // general parsing method, but in the method that parses and keeps raw_input.
// // New fields could be added upon request.
// enum CountryCodeSource {
// // Default value returned if this is not set, because the phone number was
// // created using parse, not parseAndKeepRawInput. hasCountryCodeSource will
// // return false if this is the case.
// UNSPECIFIED = 0;
//
// // The country_code is derived based on a phone number with a leading "+",
// // e.g. the French number "+33 1 42 68 53 00".
// FROM_NUMBER_WITH_PLUS_SIGN = 1;
//
// // The country_code is derived based on a phone number with a leading IDD,
// // e.g. the French number "011 33 1 42 68 53 00", as it is dialled from US.
// FROM_NUMBER_WITH_IDD = 5;
//
// // The country_code is derived based on a phone number without a leading
// // "+", e.g. the French number "33 1 42 68 53 00" when defaultCountry is
// // supplied as France.
// FROM_NUMBER_WITHOUT_PLUS_SIGN = 10;
//
// // The country_code is derived NOT based on the phone number itself, but
// // from the defaultCountry parameter provided in the parsing function by the
// // clients. This happens mostly for numbers written in the national format
// // (without country code). For example, this would be set when parsing the
// // French number "01 42 68 53 00", when defaultCountry is supplied as
// // France.
// FROM_DEFAULT_COUNTRY = 20;
// }

View File

@@ -0,0 +1,20 @@
import extractCountryCallingCode from './extractCountryCallingCode.js'
import metadata from '../../metadata.min.json' assert { type: 'json' }
describe('extractCountryCallingCode', () => {
it('should extract country calling code from a number', () => {
extractCountryCallingCode('+78005553535', null, null, metadata).should.deep.equal({
countryCallingCodeSource: 'FROM_NUMBER_WITH_PLUS_SIGN',
countryCallingCode: '7',
number: '8005553535'
})
extractCountryCallingCode('+7800', null, null, metadata).should.deep.equal({
countryCallingCodeSource: 'FROM_NUMBER_WITH_PLUS_SIGN',
countryCallingCode: '7',
number: '800'
})
extractCountryCallingCode('', null, null, metadata).should.deep.equal({})
})
})

View File

@@ -0,0 +1,63 @@
import Metadata from '../metadata.js'
import matchesEntirely from './matchesEntirely.js'
import extractNationalNumber from './extractNationalNumber.js'
import checkNumberLength from './checkNumberLength.js'
import getCountryCallingCode from '../getCountryCallingCode.js'
/**
* Sometimes some people incorrectly input international phone numbers
* without the leading `+`. This function corrects such input.
* @param {string} number — Phone number digits.
* @param {string?} country
* @param {string?} callingCode
* @param {object} metadata
* @return {object} `{ countryCallingCode: string?, number: string }`.
*/
export default function extractCountryCallingCodeFromInternationalNumberWithoutPlusSign(
number,
country,
callingCode,
metadata
) {
const countryCallingCode = country ? getCountryCallingCode(country, metadata) : callingCode
if (number.indexOf(countryCallingCode) === 0) {
metadata = new Metadata(metadata)
metadata.selectNumberingPlan(country, callingCode)
const possibleShorterNumber = number.slice(countryCallingCode.length)
const {
nationalNumber: possibleShorterNationalNumber,
} = extractNationalNumber(
possibleShorterNumber,
metadata
)
const {
nationalNumber
} = extractNationalNumber(
number,
metadata
)
// If the number was not valid before but is valid now,
// or if it was too long before, we consider the number
// with the country calling code stripped to be a better result
// and keep that instead.
// For example, in Germany (+49), `49` is a valid area code,
// so if a number starts with `49`, it could be both a valid
// national German number or an international number without
// a leading `+`.
if (
(
!matchesEntirely(nationalNumber, metadata.nationalNumberPattern())
&&
matchesEntirely(possibleShorterNationalNumber, metadata.nationalNumberPattern())
)
||
checkNumberLength(nationalNumber, metadata) === 'TOO_LONG'
) {
return {
countryCallingCode,
number: possibleShorterNumber
}
}
}
return { number }
}

View File

@@ -0,0 +1,74 @@
import extractPhoneContext, {
isPhoneContextValid,
PLUS_SIGN,
RFC3966_PREFIX_,
RFC3966_PHONE_CONTEXT_,
RFC3966_ISDN_SUBADDRESS_
} from './extractPhoneContext.js'
import ParseError from '../ParseError.js'
/**
* @param {string} numberToParse
* @param {string} nationalNumber
* @return {}
*/
export default function extractFormattedPhoneNumberFromPossibleRfc3966NumberUri(numberToParse, {
extractFormattedPhoneNumber
}) {
const phoneContext = extractPhoneContext(numberToParse)
if (!isPhoneContextValid(phoneContext)) {
throw new ParseError('NOT_A_NUMBER')
}
let phoneNumberString
if (phoneContext === null) {
// Extract a possible number from the string passed in.
// (this strips leading characters that could not be the start of a phone number)
phoneNumberString = extractFormattedPhoneNumber(numberToParse) || ''
} else {
phoneNumberString = ''
// If the phone context contains a phone number prefix, we need to capture
// it, whereas domains will be ignored.
if (phoneContext.charAt(0) === PLUS_SIGN) {
phoneNumberString += phoneContext
}
// Now append everything between the "tel:" prefix and the phone-context.
// This should include the national number, an optional extension or
// isdn-subaddress component. Note we also handle the case when "tel:" is
// missing, as we have seen in some of the phone number inputs.
// In that case, we append everything from the beginning.
const indexOfRfc3966Prefix = numberToParse.indexOf(RFC3966_PREFIX_)
let indexOfNationalNumber
// RFC 3966 "tel:" prefix is preset at this stage because
// `isPhoneContextValid()` requires it to be present.
/* istanbul ignore else */
if (indexOfRfc3966Prefix >= 0) {
indexOfNationalNumber = indexOfRfc3966Prefix + RFC3966_PREFIX_.length
} else {
indexOfNationalNumber = 0
}
const indexOfPhoneContext = numberToParse.indexOf(RFC3966_PHONE_CONTEXT_)
phoneNumberString += numberToParse.substring(indexOfNationalNumber, indexOfPhoneContext)
}
// Delete the isdn-subaddress and everything after it if it is present.
// Note extension won't appear at the same time with isdn-subaddress
// according to paragraph 5.3 of the RFC3966 spec.
const indexOfIsdn = phoneNumberString.indexOf(RFC3966_ISDN_SUBADDRESS_)
if (indexOfIsdn > 0) {
phoneNumberString = phoneNumberString.substring(0, indexOfIsdn)
}
// If both phone context and isdn-subaddress are absent but other
// parameters are present, the parameters are left in nationalNumber.
// This is because we are concerned about deleting content from a potential
// number string when there is no strong evidence that the number is
// actually written in RFC3966.
if (phoneNumberString !== '') {
return phoneNumberString
}
}

View File

@@ -0,0 +1,106 @@
import extractNationalNumberFromPossiblyIncompleteNumber from './extractNationalNumberFromPossiblyIncompleteNumber.js'
import matchesEntirely from './matchesEntirely.js'
import checkNumberLength from './checkNumberLength.js'
/**
* Strips national prefix and carrier code from a complete phone number.
* The difference from the non-"FromCompleteNumber" function is that
* it won't extract national prefix if the resultant number is too short
* to be a complete number for the selected phone numbering plan.
* @param {string} number — Complete phone number digits.
* @param {Metadata} metadata — Metadata with a phone numbering plan selected.
* @return {object} `{ nationalNumber: string, carrierCode: string? }`.
*/
export default function extractNationalNumber(number, metadata) {
// Parsing national prefixes and carrier codes
// is only required for local phone numbers
// but some people don't understand that
// and sometimes write international phone numbers
// with national prefixes (or maybe even carrier codes).
// http://ucken.blogspot.ru/2016/03/trunk-prefixes-in-skype4b.html
// Google's original library forgives such mistakes
// and so does this library, because it has been requested:
// https://github.com/catamphetamine/libphonenumber-js/issues/127
const {
carrierCode,
nationalNumber
} = extractNationalNumberFromPossiblyIncompleteNumber(
number,
metadata
)
if (nationalNumber !== number) {
if (!shouldHaveExtractedNationalPrefix(number, nationalNumber, metadata)) {
// Don't strip the national prefix.
return { nationalNumber: number }
}
// Check the national (significant) number length after extracting national prefix and carrier code.
// Legacy generated metadata (before `1.0.18`) didn't support the "possible lengths" feature.
if (metadata.possibleLengths()) {
// The number remaining after stripping the national prefix and carrier code
// should be long enough to have a possible length for the country.
// Otherwise, don't strip the national prefix and carrier code,
// since the original number could be a valid number.
// This check has been copy-pasted "as is" from Google's original library:
// https://github.com/google/libphonenumber/blob/876268eb1ad6cdc1b7b5bef17fc5e43052702d57/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java#L3236-L3250
// It doesn't check for the "possibility" of the original `number`.
// I guess it's fine not checking that one. It works as is anyway.
if (!isPossibleIncompleteNationalNumber(nationalNumber, metadata)) {
// Don't strip the national prefix.
return { nationalNumber: number }
}
}
}
return { nationalNumber, carrierCode }
}
// In some countries, the same digit could be a national prefix
// or a leading digit of a valid phone number.
// For example, in Russia, national prefix is `8`,
// and also `800 555 35 35` is a valid number
// in which `8` is not a national prefix, but the first digit
// of a national (significant) number.
// Same's with Belarus:
// `82004910060` is a valid national (significant) number,
// but `2004910060` is not.
// To support such cases (to prevent the code from always stripping
// national prefix), a condition is imposed: a national prefix
// is not extracted when the original number is "viable" and the
// resultant number is not, a "viable" national number being the one
// that matches `national_number_pattern`.
function shouldHaveExtractedNationalPrefix(nationalNumberBefore, nationalNumberAfter, metadata) {
// The equivalent in Google's code is:
// https://github.com/google/libphonenumber/blob/e326fa1fc4283bb05eb35cb3c15c18f98a31af33/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java#L2969-L3004
if (matchesEntirely(nationalNumberBefore, metadata.nationalNumberPattern()) &&
!matchesEntirely(nationalNumberAfter, metadata.nationalNumberPattern())) {
return false
}
// This "is possible" national number (length) check has been commented out
// because it's superceded by the (effectively) same check done in the
// `extractNationalNumber()` function after it calls `shouldHaveExtractedNationalPrefix()`.
// In other words, why run the same check twice if it could only be run once.
// // Check the national (significant) number length after extracting national prefix and carrier code.
// // Fixes a minor "weird behavior" bug: https://gitlab.com/catamphetamine/libphonenumber-js/-/issues/57
// // (Legacy generated metadata (before `1.0.18`) didn't support the "possible lengths" feature).
// if (metadata.possibleLengths()) {
// if (isPossibleIncompleteNationalNumber(nationalNumberBefore, metadata) &&
// !isPossibleIncompleteNationalNumber(nationalNumberAfter, metadata)) {
// return false
// }
// }
return true
}
function isPossibleIncompleteNationalNumber(nationalNumber, metadata) {
switch (checkNumberLength(nationalNumber, metadata)) {
case 'TOO_SHORT':
case 'INVALID_LENGTH':
// This library ignores "local-only" phone numbers (for simplicity).
// See the readme for more info on what are "local-only" phone numbers.
// case 'IS_POSSIBLE_LOCAL_ONLY':
return false
default:
return true
}
}

View File

@@ -0,0 +1,15 @@
import extractNationalNumber from './extractNationalNumber.js'
import Metadata from '../metadata.js'
import oldMetadata from '../../test/metadata/1.0.0/metadata.min.json' assert { type: 'json' }
describe('extractNationalNumber', function() {
it('should extract a national number when using old metadata', function() {
const _oldMetadata = new Metadata(oldMetadata)
_oldMetadata.selectNumberingPlan('RU')
extractNationalNumber('88005553535', _oldMetadata).should.deep.equal({
nationalNumber: '8005553535',
carrierCode: undefined
})
})
})

View File

@@ -0,0 +1,104 @@
/**
* Strips any national prefix (such as 0, 1) present in a
* (possibly incomplete) number provided.
* "Carrier codes" are only used in Colombia and Brazil,
* and only when dialing within those countries from a mobile phone to a fixed line number.
* Sometimes it won't actually strip national prefix
* and will instead prepend some digits to the `number`:
* for example, when number `2345678` is passed with `VI` country selected,
* it will return `{ number: "3402345678" }`, because `340` area code is prepended.
* @param {string} number — National number digits.
* @param {object} metadata — Metadata with country selected.
* @return {object} `{ nationalNumber: string, nationalPrefix: string? carrierCode: string? }`. Even if a national prefix was extracted, it's not necessarily present in the returned object, so don't rely on its presence in the returned object in order to find out whether a national prefix has been extracted or not.
*/
export default function extractNationalNumberFromPossiblyIncompleteNumber(number, metadata) {
if (number && metadata.numberingPlan.nationalPrefixForParsing()) {
// See METADATA.md for the description of
// `national_prefix_for_parsing` and `national_prefix_transform_rule`.
// Attempt to parse the first digits as a national prefix.
const prefixPattern = new RegExp('^(?:' + metadata.numberingPlan.nationalPrefixForParsing() + ')')
const prefixMatch = prefixPattern.exec(number)
if (prefixMatch) {
let nationalNumber
let carrierCode
// https://gitlab.com/catamphetamine/libphonenumber-js/-/blob/master/METADATA.md#national_prefix_for_parsing--national_prefix_transform_rule
// If a `national_prefix_for_parsing` has any "capturing groups"
// then it means that the national (significant) number is equal to
// those "capturing groups" transformed via `national_prefix_transform_rule`,
// and nothing could be said about the actual national prefix:
// what is it and was it even there.
// If a `national_prefix_for_parsing` doesn't have any "capturing groups",
// then everything it matches is a national prefix.
// To determine whether `national_prefix_for_parsing` matched any
// "capturing groups", the value of the result of calling `.exec()`
// is looked at, and if it has non-undefined values where there're
// "capturing groups" in the regular expression, then it means
// that "capturing groups" have been matched.
// It's not possible to tell whether there'll be any "capturing gropus"
// before the matching process, because a `national_prefix_for_parsing`
// could exhibit both behaviors.
const capturedGroupsCount = prefixMatch.length - 1
const hasCapturedGroups = capturedGroupsCount > 0 && prefixMatch[capturedGroupsCount]
if (metadata.nationalPrefixTransformRule() && hasCapturedGroups) {
nationalNumber = number.replace(
prefixPattern,
metadata.nationalPrefixTransformRule()
)
// If there's more than one captured group,
// then carrier code is the second one.
if (capturedGroupsCount > 1) {
carrierCode = prefixMatch[1]
}
}
// If there're no "capturing groups",
// or if there're "capturing groups" but no
// `national_prefix_transform_rule`,
// then just strip the national prefix from the number,
// and possibly a carrier code.
// Seems like there could be more.
else {
// `prefixBeforeNationalNumber` is the whole substring matched by
// the `national_prefix_for_parsing` regular expression.
// There seem to be no guarantees that it's just a national prefix.
// For example, if there's a carrier code, it's gonna be a
// part of `prefixBeforeNationalNumber` too.
const prefixBeforeNationalNumber = prefixMatch[0]
nationalNumber = number.slice(prefixBeforeNationalNumber.length)
// If there's at least one captured group,
// then carrier code is the first one.
if (hasCapturedGroups) {
carrierCode = prefixMatch[1]
}
}
// Tries to guess whether a national prefix was present in the input.
// This is not something copy-pasted from Google's library:
// they don't seem to have an equivalent for that.
// So this isn't an "officially approved" way of doing something like that.
// But since there seems no other existing method, this library uses it.
let nationalPrefix
if (hasCapturedGroups) {
const possiblePositionOfTheFirstCapturedGroup = number.indexOf(prefixMatch[1])
const possibleNationalPrefix = number.slice(0, possiblePositionOfTheFirstCapturedGroup)
// Example: an Argentinian (AR) phone number `0111523456789`.
// `prefixMatch[0]` is `01115`, and `$1` is `11`,
// and the rest of the phone number is `23456789`.
// The national number is transformed via `9$1` to `91123456789`.
// National prefix `0` is detected being present at the start.
// if (possibleNationalPrefix.indexOf(metadata.numberingPlan.nationalPrefix()) === 0) {
if (possibleNationalPrefix === metadata.numberingPlan.nationalPrefix()) {
nationalPrefix = metadata.numberingPlan.nationalPrefix()
}
} else {
nationalPrefix = prefixMatch[0]
}
return {
nationalNumber,
nationalPrefix,
carrierCode
}
}
}
return {
nationalNumber: number
}
}

View File

@@ -0,0 +1,15 @@
import Metadata from '../metadata.js'
import metadata from '../../metadata.min.json' assert { type: 'json' }
import extractNationalNumberFromPossiblyIncompleteNumber from './extractNationalNumberFromPossiblyIncompleteNumber.js'
describe('extractNationalNumberFromPossiblyIncompleteNumber', () => {
it('should parse a carrier code when there is no national prefix transform rule', () => {
const meta = new Metadata(metadata)
meta.country('AU')
extractNationalNumberFromPossiblyIncompleteNumber('18311800123', meta).should.deep.equal({
nationalPrefix: undefined,
carrierCode: '1831',
nationalNumber: '1800123'
})
})
})

View File

@@ -0,0 +1,103 @@
// When phone numbers are written in `RFC3966` format — `"tel:+12133734253"` —
// they can have their "calling code" part written separately in a `phone-context` parameter.
// Example: `"tel:12133734253;phone-context=+1"`.
// This function parses the full phone number from the local number and the `phone-context`
// when the `phone-context` contains a `+` sign.
import {
VALID_DIGITS,
// PLUS_CHARS
} from '../constants.js'
export const PLUS_SIGN = '+'
const RFC3966_VISUAL_SEPARATOR_ = '[\\-\\.\\(\\)]?'
const RFC3966_PHONE_DIGIT_ = '(' + '[' + VALID_DIGITS + ']' + '|' + RFC3966_VISUAL_SEPARATOR_ + ')'
const RFC3966_GLOBAL_NUMBER_DIGITS_ =
'^' +
'\\' +
PLUS_SIGN +
RFC3966_PHONE_DIGIT_ +
'*' +
'[' + VALID_DIGITS + ']' +
RFC3966_PHONE_DIGIT_ +
'*' +
'$'
/**
* Regular expression of valid global-number-digits for the phone-context
* parameter, following the syntax defined in RFC3966.
*/
const RFC3966_GLOBAL_NUMBER_DIGITS_PATTERN_ = new RegExp(RFC3966_GLOBAL_NUMBER_DIGITS_, 'g')
// In this port of Google's library, we don't accept alpha characters in phone numbers.
// const ALPHANUM_ = VALID_ALPHA_ + VALID_DIGITS
const ALPHANUM_ = VALID_DIGITS
const RFC3966_DOMAINLABEL_ = '[' + ALPHANUM_ + ']+((\\-)*[' + ALPHANUM_ + '])*'
const VALID_ALPHA_ = 'a-zA-Z'
const RFC3966_TOPLABEL_ = '[' + VALID_ALPHA_ + ']+((\\-)*[' + ALPHANUM_ + '])*'
const RFC3966_DOMAINNAME_ = '^(' + RFC3966_DOMAINLABEL_ + '\\.)*' + RFC3966_TOPLABEL_ + '\\.?$'
/**
* Regular expression of valid domainname for the phone-context parameter,
* following the syntax defined in RFC3966.
*/
const RFC3966_DOMAINNAME_PATTERN_ = new RegExp(RFC3966_DOMAINNAME_, 'g')
export const RFC3966_PREFIX_ = 'tel:'
export const RFC3966_PHONE_CONTEXT_ = ';phone-context='
export const RFC3966_ISDN_SUBADDRESS_ = ';isub='
/**
* Extracts the value of the phone-context parameter of `numberToExtractFrom`,
* following the syntax defined in RFC3966.
*
* @param {string} numberToExtractFrom
* @return {string|null} the extracted string (possibly empty), or `null` if no phone-context parameter is found.
*/
export default function extractPhoneContext(numberToExtractFrom) {
const indexOfPhoneContext = numberToExtractFrom.indexOf(RFC3966_PHONE_CONTEXT_)
// If no phone-context parameter is present
if (indexOfPhoneContext < 0) {
return null
}
const phoneContextStart = indexOfPhoneContext + RFC3966_PHONE_CONTEXT_.length
// If phone-context parameter is empty
if (phoneContextStart >= numberToExtractFrom.length) {
return ''
}
const phoneContextEnd = numberToExtractFrom.indexOf(';', phoneContextStart)
// If phone-context is not the last parameter
if (phoneContextEnd >= 0) {
return numberToExtractFrom.substring(phoneContextStart, phoneContextEnd)
} else {
return numberToExtractFrom.substring(phoneContextStart)
}
}
/**
* Returns whether the value of phoneContext follows the syntax defined in RFC3966.
*
* @param {string|null} phoneContext
* @return {boolean}
*/
export function isPhoneContextValid(phoneContext) {
if (phoneContext === null) {
return true
}
if (phoneContext.length === 0) {
return false
}
// Does phone-context value match pattern of global-number-digits or domainname.
return RFC3966_GLOBAL_NUMBER_DIGITS_PATTERN_.test(phoneContext) ||
RFC3966_DOMAINNAME_PATTERN_.test(phoneContext)
}

View File

@@ -0,0 +1,104 @@
import parsePhoneNumber_ from '../parsePhoneNumber.js'
import PhoneNumber from '../PhoneNumber.js'
import metadata from '../../metadata.min.json' assert { type: 'json' }
function parsePhoneNumber(...parameters) {
parameters.push(metadata)
return parsePhoneNumber_.apply(this, parameters)
}
describe('extractPhoneContext', function() {
it('should parse RFC 3966 phone number URIs', function() {
// context = ";phone-context=" descriptor
// descriptor = domainname / global-number-digits
const NZ_NUMBER = new PhoneNumber('64', '33316005', metadata)
// Valid global-phone-digits
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;phone-context=+64'),
NZ_NUMBER
)
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;phone-context=+64;{this isn\'t part of phone-context anymore!}'),
NZ_NUMBER
)
const nzFromPhoneContext = new PhoneNumber('64', '3033316005', metadata)
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;phone-context=+64-3'),
nzFromPhoneContext
)
const brFromPhoneContext = new PhoneNumber('55', '5033316005', metadata)
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;phone-context=+(555)'),
brFromPhoneContext
)
const usFromPhoneContext = new PhoneNumber('1', '23033316005', metadata)
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;phone-context=+-1-2.3()'),
usFromPhoneContext
)
// Valid domainname.
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;phone-context=abc.nz', 'NZ'),
NZ_NUMBER
)
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;phone-context=www.PHONE-numb3r.com', 'NZ'),
NZ_NUMBER
)
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;phone-context=a', 'NZ'),
NZ_NUMBER
)
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;phone-context=3phone.J.', 'NZ'),
NZ_NUMBER
)
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;phone-context=a--z', 'NZ'),
NZ_NUMBER
)
// Should strip ISDN subaddress.
expectPhoneNumbersToBeEqual(
parsePhoneNumber('tel:033316005;isub=/@;phone-context=+64', 'NZ'),
NZ_NUMBER
)
// // Should support incorrectly-written RFC 3966 phone numbers:
// // the ones written without a `tel:` prefix.
// expectPhoneNumbersToBeEqual(
// parsePhoneNumber('033316005;phone-context=+64', 'NZ'),
// NZ_NUMBER
// )
// Invalid descriptor.
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=')
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=+')
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=64')
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=++64')
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=+abc')
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=.')
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=3phone')
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=a-.nz')
expectToThrowForInvalidPhoneContext('tel:033316005;phone-context=a{b}c')
})
})
function expectToThrowForInvalidPhoneContext(string) {
expect(parsePhoneNumber(string)).to.be.undefined
}
function expectPhoneNumbersToBeEqual(phoneNumber1, phoneNumber2) {
if (!phoneNumber1 || !phoneNumber2) {
return false
}
return phoneNumber1.number === phoneNumber2.number &&
phoneNumber1.ext === phoneNumber2.ext
}

View File

@@ -0,0 +1,46 @@
import applyInternationalSeparatorStyle from './applyInternationalSeparatorStyle.js'
// This was originally set to $1 but there are some countries for which the
// first group is not used in the national pattern (e.g. Argentina) so the $1
// group does not match correctly. Therefore, we use `\d`, so that the first
// group actually used in the pattern will be matched.
export const FIRST_GROUP_PATTERN = /(\$\d)/
export default function formatNationalNumberUsingFormat(
number,
format,
{
useInternationalFormat,
withNationalPrefix,
carrierCode,
metadata
}
) {
const formattedNumber = number.replace(
new RegExp(format.pattern()),
useInternationalFormat
? format.internationalFormat()
: (
// This library doesn't use `domestic_carrier_code_formatting_rule`,
// because that one is only used when formatting phone numbers
// for dialing from a mobile phone, and this is not a dialing library.
// carrierCode && format.domesticCarrierCodeFormattingRule()
// // First, replace the $CC in the formatting rule with the desired carrier code.
// // Then, replace the $FG in the formatting rule with the first group
// // and the carrier code combined in the appropriate way.
// ? format.format().replace(FIRST_GROUP_PATTERN, format.domesticCarrierCodeFormattingRule().replace('$CC', carrierCode))
// : (
// withNationalPrefix && format.nationalPrefixFormattingRule()
// ? format.format().replace(FIRST_GROUP_PATTERN, format.nationalPrefixFormattingRule())
// : format.format()
// )
withNationalPrefix && format.nationalPrefixFormattingRule()
? format.format().replace(FIRST_GROUP_PATTERN, format.nationalPrefixFormattingRule())
: format.format()
)
)
if (useInternationalFormat) {
return applyInternationalSeparatorStyle(formattedNumber)
}
return formattedNumber
}

View File

@@ -0,0 +1,30 @@
import getCountryByNationalNumber from './getCountryByNationalNumber.js'
const USE_NON_GEOGRAPHIC_COUNTRY_CODE = false
export default function getCountryByCallingCode(callingCode, {
nationalNumber: nationalPhoneNumber,
defaultCountry,
metadata
}) {
/* istanbul ignore if */
if (USE_NON_GEOGRAPHIC_COUNTRY_CODE) {
if (metadata.isNonGeographicCallingCode(callingCode)) {
return '001'
}
}
const possibleCountries = metadata.getCountryCodesForCallingCode(callingCode)
if (!possibleCountries) {
return
}
// If there's just one country corresponding to the country code,
// then just return it, without further phone number digits validation.
if (possibleCountries.length === 1) {
return possibleCountries[0]
}
return getCountryByNationalNumber(nationalPhoneNumber, {
countries: possibleCountries,
defaultCountry,
metadata: metadata.metadata
})
}

View File

@@ -0,0 +1,52 @@
import Metadata from '../metadata.js'
import getNumberType from './getNumberType.js'
export default function getCountryByNationalNumber(nationalPhoneNumber, {
countries,
defaultCountry,
metadata
}) {
// Re-create `metadata` because it will be selecting a `country`.
metadata = new Metadata(metadata)
// const matchingCountries = []
for (const country of countries) {
metadata.country(country)
// "Leading digits" patterns are only defined for about 20% of all countries.
// By definition, matching "leading digits" is a sufficient but not a necessary
// condition for a phone number to belong to a country.
// The point of "leading digits" check is that it's the fastest one to get a match.
// https://gitlab.com/catamphetamine/libphonenumber-js/blob/master/METADATA.md#leading_digits
// I'd suppose that "leading digits" patterns are mutually exclusive for different countries
// because of the intended use of that feature.
if (metadata.leadingDigits()) {
if (nationalPhoneNumber &&
nationalPhoneNumber.search(metadata.leadingDigits()) === 0) {
return country
}
}
// Else perform full validation with all of those
// fixed-line/mobile/etc regular expressions.
else if (getNumberType({ phone: nationalPhoneNumber, country }, undefined, metadata.metadata)) {
// If both the `defaultCountry` and the "main" one match the phone number,
// don't prefer the `defaultCountry` over the "main" one.
// https://gitlab.com/catamphetamine/libphonenumber-js/-/issues/154
return country
// // If the `defaultCountry` is among the `matchingCountries` then return it.
// if (defaultCountry) {
// if (country === defaultCountry) {
// return country
// }
// matchingCountries.push(country)
// } else {
// return country
// }
}
}
// // Return the first ("main") one of the `matchingCountries`.
// if (matchingCountries.length > 0) {
// return matchingCountries[0]
// }
}

View File

@@ -0,0 +1,25 @@
import Metadata from '../metadata.js'
/**
* Pattern that makes it easy to distinguish whether a region has a single
* international dialing prefix or not. If a region has a single international
* prefix (e.g. 011 in USA), it will be represented as a string that contains
* a sequence of ASCII digits, and possibly a tilde, which signals waiting for
* the tone. If there are multiple available international prefixes in a
* region, they will be represented as a regex string that always contains one
* or more characters that are not ASCII digits or a tilde.
*/
const SINGLE_IDD_PREFIX_REG_EXP = /^[\d]+(?:[~\u2053\u223C\uFF5E][\d]+)?$/
// For regions that have multiple IDD prefixes
// a preferred IDD prefix is returned.
export default function getIddPrefix(country, callingCode, metadata) {
const countryMetadata = new Metadata(metadata)
countryMetadata.selectNumberingPlan(country, callingCode)
if (countryMetadata.defaultIDDPrefix()) {
return countryMetadata.defaultIDDPrefix()
}
if (SINGLE_IDD_PREFIX_REG_EXP.test(countryMetadata.IDDPrefix())) {
return countryMetadata.IDDPrefix()
}
}

View File

@@ -0,0 +1,98 @@
import Metadata from '../metadata.js'
import matchesEntirely from './matchesEntirely.js'
const NON_FIXED_LINE_PHONE_TYPES = [
'MOBILE',
'PREMIUM_RATE',
'TOLL_FREE',
'SHARED_COST',
'VOIP',
'PERSONAL_NUMBER',
'PAGER',
'UAN',
'VOICEMAIL'
]
// Finds out national phone number type (fixed line, mobile, etc)
export default function getNumberType(input, options, metadata)
{
// If assigning the `{}` default value is moved to the arguments above,
// code coverage would decrease for some weird reason.
options = options || {}
// When `parse()` returns an empty object — `{}` —
// that means that the phone number is malformed,
// so it can't possibly be valid.
if (!input.country && !input.countryCallingCode) {
return
}
metadata = new Metadata(metadata)
metadata.selectNumberingPlan(input.country, input.countryCallingCode)
const nationalNumber = options.v2 ? input.nationalNumber : input.phone
// The following is copy-pasted from the original function:
// https://github.com/googlei18n/libphonenumber/blob/3ea547d4fbaa2d0b67588904dfa5d3f2557c27ff/javascript/i18n/phonenumbers/phonenumberutil.js#L2835
// Is this national number even valid for this country
if (!matchesEntirely(nationalNumber, metadata.nationalNumberPattern())) {
return
}
// Is it fixed line number
if (isNumberTypeEqualTo(nationalNumber, 'FIXED_LINE', metadata)) {
// Because duplicate regular expressions are removed
// to reduce metadata size, if "mobile" pattern is ""
// then it means it was removed due to being a duplicate of the fixed-line pattern.
//
if (metadata.type('MOBILE') && metadata.type('MOBILE').pattern() === '') {
return 'FIXED_LINE_OR_MOBILE'
}
// `MOBILE` type pattern isn't included if it matched `FIXED_LINE` one.
// For example, for "US" country.
// Old metadata (< `1.0.18`) had a specific "types" data structure
// that happened to be `undefined` for `MOBILE` in that case.
// Newer metadata (>= `1.0.18`) has another data structure that is
// not `undefined` for `MOBILE` in that case (it's just an empty array).
// So this `if` is just for backwards compatibility with old metadata.
if (!metadata.type('MOBILE')) {
return 'FIXED_LINE_OR_MOBILE'
}
// Check if the number happens to qualify as both fixed line and mobile.
// (no such country in the minimal metadata set)
/* istanbul ignore if */
if (isNumberTypeEqualTo(nationalNumber, 'MOBILE', metadata)) {
return 'FIXED_LINE_OR_MOBILE'
}
return 'FIXED_LINE'
}
for (const type of NON_FIXED_LINE_PHONE_TYPES) {
if (isNumberTypeEqualTo(nationalNumber, type, metadata)) {
return type
}
}
}
export function isNumberTypeEqualTo(nationalNumber, type, metadata) {
type = metadata.type(type)
if (!type || !type.pattern()) {
return false
}
// Check if any possible number lengths are present;
// if so, we use them to avoid checking
// the validation pattern if they don't match.
// If they are absent, this means they match
// the general description, which we have
// already checked before a specific number type.
if (type.possibleLengths() &&
type.possibleLengths().indexOf(nationalNumber.length) < 0) {
return false
}
return matchesEntirely(nationalNumber, type.pattern())
}

View File

@@ -0,0 +1,26 @@
import getNumberType from './getNumberType.js'
import oldMetadata from '../../test/metadata/1.0.0/metadata.min.json' assert { type: 'json' }
import Metadata from '../metadata.js'
describe('getNumberType', function() {
it('should get number type when using old metadata', function() {
getNumberType(
{
nationalNumber: '2133734253',
country: 'US'
},
{ v2: true },
oldMetadata
).should.equal('FIXED_LINE_OR_MOBILE')
})
it('should return `undefined` when the phone number is a malformed one', function() {
expect(getNumberType(
{},
{ v2: true },
oldMetadata
)).to.equal(undefined)
})
})

View File

@@ -0,0 +1,28 @@
import Metadata from '../metadata.js'
/**
* Returns a list of countries that the phone number could potentially belong to.
* @param {string} callingCode — Calling code.
* @param {string} nationalNumber — National (significant) number.
* @param {object} metadata — Metadata.
* @return {string[]} A list of possible countries.
*/
export default function getPossibleCountriesForNumber(callingCode, nationalNumber, metadata) {
const _metadata = new Metadata(metadata)
let possibleCountries = _metadata.getCountryCodesForCallingCode(callingCode)
if (!possibleCountries) {
return []
}
return possibleCountries.filter((country) => {
return couldNationalNumberBelongToCountry(nationalNumber, country, metadata)
})
}
function couldNationalNumberBelongToCountry(nationalNumber, country, metadata) {
const _metadata = new Metadata(metadata)
_metadata.selectNumberingPlan(country)
if (_metadata.numberingPlan.possibleLengths().indexOf(nationalNumber.length) >= 0) {
return true
}
return false
}

View File

@@ -0,0 +1,5 @@
const objectConstructor = {}.constructor;
export default function isObject(object) {
return object !== undefined && object !== null && object.constructor === objectConstructor;
}

View File

@@ -0,0 +1,108 @@
import {
MIN_LENGTH_FOR_NSN,
VALID_DIGITS,
VALID_PUNCTUATION,
PLUS_CHARS
} from '../constants.js'
import createExtensionPattern from './extension/createExtensionPattern.js'
// 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)
//
const 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)
//
export const VALID_PHONE_NUMBER =
'[' + PLUS_CHARS + ']{0,1}' +
'(?:' +
'[' + VALID_PUNCTUATION + ']*' +
'[' + VALID_DIGITS + ']' +
'){3,}' +
'[' +
VALID_PUNCTUATION +
VALID_DIGITS +
']*'
// This regular expression isn't present in Google's `libphonenumber`
// and is only used to determine whether the phone number being input
// is too short for it to even consider it a "valid" number.
// This is just a way to differentiate between a really invalid phone
// number like "abcde" and a valid phone number that a user has just
// started inputting, like "+1" or "1": both these cases would be
// considered `NOT_A_NUMBER` by Google's `libphonenumber`, but this
// library can provide a more detailed error message — whether it's
// really "not a number", or is it just a start of a valid phone number.
const VALID_PHONE_NUMBER_START_REG_EXP = new RegExp(
'^' +
'[' + PLUS_CHARS + ']{0,1}' +
'(?:' +
'[' + VALID_PUNCTUATION + ']*' +
'[' + VALID_DIGITS + ']' +
'){1,2}' +
'$'
, 'i')
export const VALID_PHONE_NUMBER_WITH_EXTENSION =
VALID_PHONE_NUMBER +
// Phone number extensions
'(?:' + createExtensionPattern() + ')?'
// The combined regular expression for valid phone numbers:
//
const 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_WITH_EXTENSION +
'$'
, 'i')
// 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`.
//
export default function isViablePhoneNumber(number) {
return number.length >= MIN_LENGTH_FOR_NSN &&
VALID_PHONE_NUMBER_PATTERN.test(number)
}
// This is just a way to differentiate between a really invalid phone
// number like "abcde" and a valid phone number that a user has just
// started inputting, like "+1" or "1": both these cases would be
// considered `NOT_A_NUMBER` by Google's `libphonenumber`, but this
// library can provide a more detailed error message — whether it's
// really "not a number", or is it just a start of a valid phone number.
export function isViablePhoneNumberStart(number) {
return VALID_PHONE_NUMBER_START_REG_EXP.test(number)
}

View File

@@ -0,0 +1,11 @@
/**
* Checks whether the entire input sequence can be matched
* against the regular expression.
* @return {boolean}
*/
export default function matchesEntirely(text, regular_expression) {
// If assigning the `''` default value is moved to the arguments above,
// code coverage would decrease for some weird reason.
text = text || ''
return new RegExp('^(?:' + regular_expression + ')$').test(text)
}

View File

@@ -0,0 +1,11 @@
import matchesEntirely from './matchesEntirely.js'
describe('matchesEntirely', () => {
it('should work in edge cases', () => {
// No text.
matchesEntirely(undefined, '').should.equal(true)
// "OR" in regexp.
matchesEntirely('911231231', '4\d{8}|[1-9]\d{7}').should.equal(false)
})
})

View File

@@ -0,0 +1,24 @@
/**
* Merges two arrays.
* @param {*} a
* @param {*} b
* @return {*}
*/
export default function mergeArrays(a, b) {
const merged = a.slice()
for (const element of b) {
if (a.indexOf(element) < 0) {
merged.push(element)
}
}
return merged.sort((a, b) => a - b)
// ES6 version, requires Set polyfill.
// let merged = new Set(a)
// for (const element of b) {
// merged.add(i)
// }
// return Array.from(merged).sort((a, b) => a - b)
}

View File

@@ -0,0 +1,7 @@
import mergeArrays from './mergeArrays.js'
describe('mergeArrays', () => {
it('should merge arrays', () => {
mergeArrays([1, 2], [2, 3]).should.deep.equal([1, 2, 3])
})
})

View File

@@ -0,0 +1,82 @@
// These mappings map a character (key) to a specific digit that should
// replace it for normalization purposes. Non-European digits that
// may be used in phone numbers are mapped to a European equivalent.
//
// E.g. in Iraq they don't write `+442323234` but rather `+٤٤٢٣٢٣٢٣٤`.
//
export const DIGITS = {
'0': '0',
'1': '1',
'2': '2',
'3': '3',
'4': '4',
'5': '5',
'6': '6',
'7': '7',
'8': '8',
'9': '9',
'\uFF10': '0', // Fullwidth digit 0
'\uFF11': '1', // Fullwidth digit 1
'\uFF12': '2', // Fullwidth digit 2
'\uFF13': '3', // Fullwidth digit 3
'\uFF14': '4', // Fullwidth digit 4
'\uFF15': '5', // Fullwidth digit 5
'\uFF16': '6', // Fullwidth digit 6
'\uFF17': '7', // Fullwidth digit 7
'\uFF18': '8', // Fullwidth digit 8
'\uFF19': '9', // Fullwidth digit 9
'\u0660': '0', // Arabic-indic digit 0
'\u0661': '1', // Arabic-indic digit 1
'\u0662': '2', // Arabic-indic digit 2
'\u0663': '3', // Arabic-indic digit 3
'\u0664': '4', // Arabic-indic digit 4
'\u0665': '5', // Arabic-indic digit 5
'\u0666': '6', // Arabic-indic digit 6
'\u0667': '7', // Arabic-indic digit 7
'\u0668': '8', // Arabic-indic digit 8
'\u0669': '9', // Arabic-indic digit 9
'\u06F0': '0', // Eastern-Arabic digit 0
'\u06F1': '1', // Eastern-Arabic digit 1
'\u06F2': '2', // Eastern-Arabic digit 2
'\u06F3': '3', // Eastern-Arabic digit 3
'\u06F4': '4', // Eastern-Arabic digit 4
'\u06F5': '5', // Eastern-Arabic digit 5
'\u06F6': '6', // Eastern-Arabic digit 6
'\u06F7': '7', // Eastern-Arabic digit 7
'\u06F8': '8', // Eastern-Arabic digit 8
'\u06F9': '9' // Eastern-Arabic digit 9
}
export function parseDigit(character) {
return DIGITS[character]
}
/**
* Parses phone number digits from a string.
* Drops all punctuation leaving only digits.
* Also converts wide-ascii and arabic-indic numerals to conventional numerals.
* E.g. in Iraq they don't write `+442323234` but rather `+٤٤٢٣٢٣٢٣٤`.
* @param {string} string
* @return {string}
* @example
* ```js
* parseDigits('8 (800) 555')
* // Outputs '8800555'.
* ```
*/
export default function parseDigits(string) {
let result = ''
// Using `.split('')` here instead of normal `for ... of`
// because the importing application doesn't neccessarily include an ES6 polyfill.
// The `.split('')` approach discards "exotic" UTF-8 characters
// (the ones consisting of four bytes) but digits
// (including non-European ones) don't fall into that range
// so such "exotic" characters would be discarded anyway.
for (const character of string.split('')) {
const digit = parseDigit(character)
if (digit) {
result += digit
}
}
return result
}

View File

@@ -0,0 +1,7 @@
import parseDigits from './parseDigits.js'
describe('parseDigits', () => {
it('should parse digits', () => {
parseDigits('+٤٤٢٣٢٣٢٣٤').should.equal('442323234')
})
})

View File

@@ -0,0 +1,30 @@
import Metadata from '../metadata.js'
import { VALID_DIGITS } from '../constants.js'
const CAPTURING_DIGIT_PATTERN = new RegExp('([' + VALID_DIGITS + '])')
export default function stripIddPrefix(number, country, callingCode, metadata) {
if (!country) {
return
}
// Check if the number is IDD-prefixed.
const countryMetadata = new Metadata(metadata)
countryMetadata.selectNumberingPlan(country, callingCode)
const IDDPrefixPattern = new RegExp(countryMetadata.IDDPrefix())
if (number.search(IDDPrefixPattern) !== 0) {
return
}
// Strip IDD prefix.
number = number.slice(number.match(IDDPrefixPattern)[0].length)
// If there're any digits after an IDD prefix,
// then those digits are a country calling code.
// Since no country code starts with a `0`,
// the code below validates that the next digit (if present) is not `0`.
const matchedGroups = number.match(CAPTURING_DIGIT_PATTERN)
if (matchedGroups && matchedGroups[1] != null && matchedGroups[1].length > 0) {
if (matchedGroups[1] === '0') {
return
}
}
return number
}

View File

@@ -0,0 +1,21 @@
import stripIddPrefix from './stripIddPrefix.js'
import metadata from '../../metadata.min.json' assert { type: 'json' }
describe('stripIddPrefix', () => {
it('should strip a valid IDD prefix', () => {
stripIddPrefix('01178005553535', 'US', '1', metadata).should.equal('78005553535')
})
it('should strip a valid IDD prefix (no country calling code)', () => {
stripIddPrefix('011', 'US', '1', metadata).should.equal('')
})
it('should strip a valid IDD prefix (valid country calling code)', () => {
stripIddPrefix('0117', 'US', '1', metadata).should.equal('7')
})
it('should strip a valid IDD prefix (not a valid country calling code)', () => {
expect(stripIddPrefix('0110', 'US', '1', metadata)).to.be.undefined
})
})