feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,122 @@
import parse from '../parse.js'
import { VALID_PHONE_NUMBER_WITH_EXTENSION } from '../helpers/isViablePhoneNumber.js'
import parsePreCandidate from '../findNumbers/parsePreCandidate.js'
import isValidPreCandidate from '../findNumbers/isValidPreCandidate.js'
import isValidCandidate from '../findNumbers/isValidCandidate.js'
import {
// PLUS_CHARS,
VALID_PUNCTUATION,
// VALID_DIGITS,
WHITESPACE
} from '../constants.js'
const WHITESPACE_IN_THE_BEGINNING_PATTERN = new RegExp('^[' + WHITESPACE + ']+')
const PUNCTUATION_IN_THE_END_PATTERN = new RegExp('[' + VALID_PUNCTUATION + ']+$')
/**
* Extracts a parseable phone number including any opening brackets, etc.
* @param {string} text - Input.
* @return {object} `{ ?number, ?startsAt, ?endsAt }`.
*/
export default class PhoneNumberSearch {
constructor(text, options, metadata) {
this.text = text
// If assigning the `{}` default value is moved to the arguments above,
// code coverage would decrease for some weird reason.
this.options = options || {}
this.metadata = metadata
// Iteration tristate.
this.state = 'NOT_READY'
this.regexp = new RegExp(VALID_PHONE_NUMBER_WITH_EXTENSION, 'ig')
}
find() {
const matches = this.regexp.exec(this.text)
if (!matches) {
return
}
let number = matches[0]
let startsAt = matches.index
number = number.replace(WHITESPACE_IN_THE_BEGINNING_PATTERN, '')
startsAt += matches[0].length - number.length
// Fixes not parsing numbers with whitespace in the end.
// Also fixes not parsing numbers with opening parentheses in the end.
// https://github.com/catamphetamine/libphonenumber-js/issues/252
number = number.replace(PUNCTUATION_IN_THE_END_PATTERN, '')
number = parsePreCandidate(number)
const result = this.parseCandidate(number, startsAt)
if (result) {
return result
}
// Tail recursion.
// Try the next one if this one is not a valid phone number.
return this.find()
}
parseCandidate(number, startsAt) {
if (!isValidPreCandidate(number, startsAt, this.text)) {
return
}
// Don't parse phone numbers which are non-phone numbers
// due to being part of something else (e.g. a UUID).
// https://github.com/catamphetamine/libphonenumber-js/issues/213
// Copy-pasted from Google's `PhoneNumberMatcher.js` (`.parseAndValidate()`).
if (!isValidCandidate(number, startsAt, this.text, this.options.extended ? 'POSSIBLE' : 'VALID')) {
return
}
// // Prepend any opening brackets left behind by the
// // `PHONE_NUMBER_START_PATTERN` regexp.
// const text_before_number = text.slice(this.searching_from, startsAt)
// const full_number_starts_at = text_before_number.search(BEFORE_NUMBER_DIGITS_PUNCTUATION)
// if (full_number_starts_at >= 0) {
// number = text_before_number.slice(full_number_starts_at) + number
// startsAt = full_number_starts_at
// }
//
// this.searching_from = matches.lastIndex
const result = parse(number, this.options, this.metadata)
if (!result.phone) {
return
}
result.startsAt = startsAt
result.endsAt = startsAt + number.length
return result
}
hasNext() {
if (this.state === 'NOT_READY') {
this.last_match = this.find()
if (this.last_match) {
this.state = 'READY'
} else {
this.state = 'DONE'
}
}
return this.state === 'READY'
}
next() {
// Check the state and find the next match as a side-effect if necessary.
if (!this.hasNext()) {
throw new Error('No next element')
}
// Don't retain that memory any longer than necessary.
const result = this.last_match
this.last_match = null
this.state = 'NOT_READY'
return result
}
}

View File

@@ -0,0 +1,12 @@
import PhoneNumberMatcher from '../PhoneNumberMatcher.js'
import normalizeArguments from '../normalizeArguments.js'
export default function findNumbers() {
const { text, options, metadata } = normalizeArguments(arguments)
const matcher = new PhoneNumberMatcher(text, options, metadata)
const results = []
while (matcher.hasNext()) {
results.push(matcher.next())
}
return results
}

View File

@@ -0,0 +1,217 @@
import findNumbers from './findNumbers.js'
import metadata from '../../metadata.max.json' with { type: 'json' }
describe('findNumbers', () => {
it('should find numbers', () => {
expect(findNumbers('2133734253', 'US', metadata)).to.deep.equal([{
phone : '2133734253',
country : 'US',
startsAt : 0,
endsAt : 10
}])
expect(findNumbers('(213) 373-4253', 'US', metadata)).to.deep.equal([{
phone : '2133734253',
country : 'US',
startsAt : 0,
endsAt : 14
}])
expect(
findNumbers('The number is +7 (800) 555-35-35 and not (213) 373-4253 as written in the document.', 'US', metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 14,
endsAt : 32
}, {
phone : '2133734253',
country : 'US',
startsAt : 41,
endsAt : 55
}])
// Opening parenthesis issue.
// https://github.com/catamphetamine/libphonenumber-js/issues/252
expect(
findNumbers('The number is +7 (800) 555-35-35 and not (213) 373-4253 (that\'s not even in the same country!) as written in the document.', 'US', metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 14,
endsAt : 32
}, {
phone : '2133734253',
country : 'US',
startsAt : 41,
endsAt : 55
}])
// No default country.
expect(
findNumbers('The number is +7 (800) 555-35-35 as written in the document.', metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 14,
endsAt : 32
}])
// Passing `options` and default country.
expect(
findNumbers('The number is +7 (800) 555-35-35 as written in the document.', 'US', { leniency: 'VALID' }, metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 14,
endsAt : 32
}])
// Passing `options`.
expect(
findNumbers('The number is +7 (800) 555-35-35 as written in the document.', { leniency: 'VALID' }, metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 14,
endsAt : 32
}])
// Not a phone number and a phone number.
expect(
findNumbers('Digits 12 are not a number, but +7 (800) 555-35-35 is.', { leniency: 'VALID' }, metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 32,
endsAt : 50
}])
// Phone number extension.
expect(
findNumbers('Date 02/17/2018 is not a number, but +7 (800) 555-35-35 ext. 123 is.', { leniency: 'VALID' }, metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
ext : '123',
startsAt : 37,
endsAt : 64
}])
})
it('should find numbers (v2)', () => {
const phoneNumbers = findNumbers('The number is +7 (800) 555-35-35 ext. 1234 and not (213) 373-4253 as written in the document.', 'US', { v2: true }, metadata)
expect(phoneNumbers.length).to.equal(2)
expect(phoneNumbers[0].startsAt).to.equal(14)
expect(phoneNumbers[0].endsAt).to.equal(42)
expect(phoneNumbers[0].number.number).to.equal('+78005553535')
expect(phoneNumbers[0].number.nationalNumber).to.equal('8005553535')
expect(phoneNumbers[0].number.country).to.equal('RU')
expect(phoneNumbers[0].number.countryCallingCode).to.equal('7')
expect(phoneNumbers[0].number.ext).to.equal('1234')
expect(phoneNumbers[1].startsAt).to.equal(51)
expect(phoneNumbers[1].endsAt).to.equal(65)
expect(phoneNumbers[1].number.number).to.equal('+12133734253')
expect(phoneNumbers[1].number.nationalNumber).to.equal('2133734253')
expect(phoneNumbers[1].number.country).to.equal('US')
expect(phoneNumbers[1].number.countryCallingCode).to.equal('1')
})
it('shouldn\'t find non-valid numbers', () => {
// Not a valid phone number for US.
expect(findNumbers('1111111111', 'US', metadata)).to.deep.equal([])
})
it('should find non-European digits', () => {
// E.g. in Iraq they don't write `+442323234` but rather `+٤٤٢٣٢٣٢٣٤`.
expect(findNumbers('العَرَبِيَّة‎ +٤٤٣٣٣٣٣٣٣٣٣٣عَرَبِيّ‎', metadata)).to.deep.equal([{
country : 'GB',
phone : '3333333333',
startsAt : 14,
endsAt : 27
}])
})
it('should work in edge cases', () => {
let thrower
// No input
expect(findNumbers('', metadata)).to.deep.equal([])
// // No country metadata for this `require` country code
// thrower = () => findNumbers('123', 'ZZ', metadata)
// thrower.should.throw('Unknown country')
// Numerical `value`
thrower = () => findNumbers(2141111111, 'US')
expect(thrower).to.throw('A text for parsing must be a string.')
// // No metadata
// thrower = () => findNumbers('')
// thrower.should.throw('`metadata` argument not passed')
// No metadata, no default country, no phone numbers.
expect(findNumbers('')).to.deep.equal([])
})
it('should find international numbers when passed a non-existent default country', () => {
const numbers = findNumbers('Phone: +7 (800) 555 35 35. National: 8 (800) 555-55-55', { defaultCountry: 'XX', v2: true }, metadata)
expect(numbers.length).to.equal(1)
expect(numbers[0].number.nationalNumber).to.equal('8005553535')
})
it('shouldn\'t find phone numbers which are not phone numbers', () => {
// A timestamp.
expect(findNumbers('2012-01-02 08:00', 'US', metadata)).to.deep.equal([])
// A valid number (not a complete timestamp).
expect(findNumbers('2012-01-02 08', 'US', metadata)).to.deep.equal([{
country : 'US',
phone : '2012010208',
startsAt : 0,
endsAt : 13
}])
// Invalid parens.
expect(findNumbers('213(3734253', 'US', metadata)).to.deep.equal([])
// Letters after phone number.
expect(findNumbers('2133734253a', 'US', metadata)).to.deep.equal([])
// Valid phone (same as the one found in the UUID below).
expect(findNumbers('The phone number is 231354125.', 'FR', metadata)).to.deep.equal([{
country : 'FR',
phone : '231354125',
startsAt : 20,
endsAt : 29
}])
// Not a phone number (part of a UUID).
// Should parse in `{ extended: true }` mode.
const possibleNumbers = findNumbers('The UUID is CA801c26f98cd16e231354125ad046e40b.', 'FR', { extended: true }, metadata)
expect(possibleNumbers.length).to.equal(1)
expect(possibleNumbers[0].country).to.equal('FR')
expect(possibleNumbers[0].phone).to.equal('231354125')
// Not a phone number (part of a UUID).
// Shouldn't parse by default.
expect(
findNumbers('The UUID is CA801c26f98cd16e231354125ad046e40b.', 'FR', metadata)
).to.deep.equal([])
})
// https://gitlab.com/catamphetamine/libphonenumber-js/-/merge_requests/4
it('should return correct `startsAt` and `endsAt` when matching "inner" candidates in a could-be-a-candidate substring', () => {
expect(findNumbers('39945926 77200596 16533084', 'ID', metadata)).to.deep.equal([{
country: 'ID',
phone: '77200596',
startsAt: 9,
endsAt: 17
}])
})
})

View File

@@ -0,0 +1,20 @@
// This is a legacy function.
// Use `findNumbers()` instead.
import _findPhoneNumbers, { searchPhoneNumbers as _searchPhoneNumbers } from './findPhoneNumbersInitialImplementation.js'
import normalizeArguments from '../normalizeArguments.js'
export default function findPhoneNumbers()
{
const { text, options, metadata } = normalizeArguments(arguments)
return _findPhoneNumbers(text, options, metadata)
}
/**
* @return ES6 `for ... of` iterator.
*/
export function searchPhoneNumbers()
{
const { text, options, metadata } = normalizeArguments(arguments)
return _searchPhoneNumbers(text, options, metadata)
}

View File

@@ -0,0 +1,249 @@
// This is a legacy function.
// Use `findNumbers()` instead.
import findNumbers, { searchPhoneNumbers } from './findPhoneNumbers.js'
import PhoneNumberSearch from './PhoneNumberSearch.js'
import metadata from '../../metadata.min.json' with { type: 'json' }
describe('findPhoneNumbers', () => {
it('should find numbers', () => {
expect(findNumbers('2133734253', 'US', metadata)).to.deep.equal([{
phone : '2133734253',
country : 'US',
startsAt : 0,
endsAt : 10
}])
expect(findNumbers('(213) 373-4253', 'US', metadata)).to.deep.equal([{
phone : '2133734253',
country : 'US',
startsAt : 0,
endsAt : 14
}])
expect(
findNumbers('The number is +7 (800) 555-35-35 and not (213) 373-4253 as written in the document.', 'US', metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 14,
endsAt : 32
}, {
phone : '2133734253',
country : 'US',
startsAt : 41,
endsAt : 55
}])
// Opening parenthesis issue.
// https://github.com/catamphetamine/libphonenumber-js/issues/252
expect(
findNumbers('The number is +7 (800) 555-35-35 and not (213) 373-4253 (that\'s not even in the same country!) as written in the document.', 'US', metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 14,
endsAt : 32
}, {
phone : '2133734253',
country : 'US',
startsAt : 41,
endsAt : 55
}])
// No default country.
expect(
findNumbers('The number is +7 (800) 555-35-35 as written in the document.', metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 14,
endsAt : 32
}])
// Passing `options` and default country.
expect(
findNumbers('The number is +7 (800) 555-35-35 as written in the document.', 'US', { leniency: 'VALID' }, metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 14,
endsAt : 32
}])
// Passing `options`.
expect(
findNumbers('The number is +7 (800) 555-35-35 as written in the document.', { leniency: 'VALID' }, metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 14,
endsAt : 32
}])
// Not a phone number and a phone number.
expect(
findNumbers('Digits 12 are not a number, but +7 (800) 555-35-35 is.', { leniency: 'VALID' }, metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
startsAt : 32,
endsAt : 50
}])
// Phone number extension.
expect(
findNumbers('Date 02/17/2018 is not a number, but +7 (800) 555-35-35 ext. 123 is.', { leniency: 'VALID' }, metadata)
).to.deep.equal([{
phone : '8005553535',
country : 'RU',
ext : '123',
startsAt : 37,
endsAt : 64
}])
})
it('shouldn\'t find non-valid numbers', () => {
// Not a valid phone number for US.
expect(findNumbers('1111111111', 'US', metadata)).to.deep.equal([])
})
it('should find non-European digits', () => {
// E.g. in Iraq they don't write `+442323234` but rather `+٤٤٢٣٢٣٢٣٤`.
expect(findNumbers('العَرَبِيَّة‎ +٤٤٣٣٣٣٣٣٣٣٣٣عَرَبِيّ‎', metadata)).to.deep.equal([{
country : 'GB',
phone : '3333333333',
startsAt : 14,
endsAt : 27
}])
})
it('should iterate', () => {
const expected_numbers = [{
country : 'RU',
phone : '8005553535',
// number : '+7 (800) 555-35-35',
startsAt : 14,
endsAt : 32
}, {
country : 'US',
phone : '2133734253',
// number : '(213) 373-4253',
startsAt : 41,
endsAt : 55
}]
for (const number of searchPhoneNumbers('The number is +7 (800) 555-35-35 and not (213) 373-4253 as written in the document.', 'US', metadata)) {
expect(number).to.deep.equal(expected_numbers.shift())
}
expect(expected_numbers.length).to.equal(0)
})
it('should work in edge cases', () => {
let thrower
// No input
expect(findNumbers('', metadata)).to.deep.equal([])
// No country metadata for this `require` country code
thrower = () => findNumbers('123', 'ZZ', metadata)
expect(thrower).to.throw('Unknown country')
// Numerical `value`
thrower = () => findNumbers(2141111111, 'US')
expect(thrower).to.throw('A text for parsing must be a string.')
// // No metadata
// thrower = () => findNumbers('')
// thrower.should.throw('`metadata` argument not passed')
})
it('shouldn\'t find phone numbers which are not phone numbers', () => {
// A timestamp.
expect(findNumbers('2012-01-02 08:00', 'US', metadata)).to.deep.equal([])
// A valid number (not a complete timestamp).
expect(findNumbers('2012-01-02 08', 'US', metadata)).to.deep.equal([{
country : 'US',
phone : '2012010208',
startsAt : 0,
endsAt : 13
}])
// Invalid parens.
expect(findNumbers('213(3734253', 'US', metadata)).to.deep.equal([])
// Letters after phone number.
expect(findNumbers('2133734253a', 'US', metadata)).to.deep.equal([])
// Valid phone (same as the one found in the UUID below).
expect(findNumbers('The phone number is 231354125.', 'FR', metadata)).to.deep.equal([{
country : 'FR',
phone : '231354125',
startsAt : 20,
endsAt : 29
}])
// Not a phone number (part of a UUID).
// Should parse in `{ extended: true }` mode.
const possibleNumbers = findNumbers('The UUID is CA801c26f98cd16e231354125ad046e40b.', 'FR', { extended: true }, metadata)
expect(possibleNumbers.length).to.equal(3)
expect(possibleNumbers[1].country).to.equal('FR')
expect(possibleNumbers[1].phone).to.equal('231354125')
// Not a phone number (part of a UUID).
// Shouldn't parse by default.
expect(
findNumbers('The UUID is CA801c26f98cd16e231354125ad046e40b.', 'FR', metadata)
).to.deep.equal([])
})
})
describe('PhoneNumberSearch', () => {
it('should search for phone numbers', () => {
const finder = new PhoneNumberSearch('The number is +7 (800) 555-35-35 and not (213) 373-4253 as written in the document.', { defaultCountry: 'US' }, metadata)
expect(finder.hasNext()).to.equal(true)
expect(finder.next()).to.deep.equal({
country : 'RU',
phone : '8005553535',
// number : '+7 (800) 555-35-35',
startsAt : 14,
endsAt : 32
})
expect(finder.hasNext()).to.equal(true)
expect(finder.next()).to.deep.equal({
country : 'US',
phone : '2133734253',
// number : '(213) 373-4253',
startsAt : 41,
endsAt : 55
})
expect(finder.hasNext()).to.equal(false)
})
it('should search for phone numbers (no options)', () => {
const finder = new PhoneNumberSearch('The number is +7 (800) 555-35-35', undefined, metadata)
expect(finder.hasNext()).to.equal(true)
expect(finder.next()).to.deep.equal({
country : 'RU',
phone : '8005553535',
// number : '+7 (800) 555-35-35',
startsAt : 14,
endsAt : 32
})
expect(finder.hasNext()).to.equal(false)
})
it('should work in edge cases', () => {
// No options
const search = new PhoneNumberSearch('', undefined, metadata)
// No next element
let thrower = () => search.next()
expect(thrower).to.throw('No next element')
})
})

View File

@@ -0,0 +1,81 @@
// This is a legacy function.
// Use `findNumbers()` instead.
import createExtensionPattern from '../helpers/extension/createExtensionPattern.js'
import PhoneNumberSearch from './PhoneNumberSearch.js'
/**
* 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.
*/
export const EXTN_PATTERNS_FOR_PARSING = createExtensionPattern('parsing')
// // Regular expression for getting opening brackets for a valid number
// // found using `PHONE_NUMBER_START_PATTERN` for prepending those brackets to the number.
// const BEFORE_NUMBER_DIGITS_PUNCTUATION = new RegExp('[' + OPENING_BRACKETS + ']+' + '[' + WHITESPACE + ']*' + '$')
// const VALID_PRECEDING_CHARACTER_PATTERN = /[^a-zA-Z0-9]/
export default function findPhoneNumbers(text, options, metadata) {
/* istanbul ignore if */
if (options === undefined) {
options = {}
}
const search = new PhoneNumberSearch(text, options, metadata)
const phones = []
while (search.hasNext()) {
phones.push(search.next())
}
return phones
}
/**
* @return ES6 `for ... of` iterator.
*/
export function searchPhoneNumbers(text, options, metadata) {
/* istanbul ignore if */
if (options === undefined) {
options = {}
}
const search = new PhoneNumberSearch(text, options, metadata)
const iterator = () => {
return {
next: () => {
if (search.hasNext()) {
return {
done: false,
value: search.next()
}
}
return {
done: true
}
}
}
}
// This line of code didn't really work with `babel`/`istanbul`:
// for some weird reason, it showed code coverage less than 100%.
// That's because `babel`/`istanbul`, for some weird reason,
// apparently doesn't know how to properly exclude Babel polyfills from code coverage.
//
// const iterable = { [Symbol.iterator]: iterator }
const iterable = {}
iterable[Symbol.iterator] = iterator
return iterable
}

View File

@@ -0,0 +1,117 @@
import _formatNumber from '../format.js'
import parse from '../parse.js'
import isObject from '../helpers/isObject.js'
export default function formatNumber() {
const {
input,
format,
options,
metadata
} = normalizeArguments(arguments)
return _formatNumber(input, format, options, metadata)
}
// Sort out arguments
function normalizeArguments(args)
{
// This line of code appeared to not work correctly with `babel`/`istanbul`:
// for some weird reason, it caused coverage less than 100%.
// That's because `babel`/`istanbul`, for some weird reason,
// apparently doesn't know how to properly exclude Babel polyfills from code coverage.
//
// const [arg_1, arg_2, arg_3, arg_4, arg_5] = Array.prototype.slice.call(args)
const arg_1 = args[0]
const arg_2 = args[1]
const arg_3 = args[2]
const arg_4 = args[3]
const arg_5 = args[4]
let input
let format
let options
let metadata
// Sort out arguments.
// If the phone number is passed as a string.
// `format('8005553535', ...)`.
if (typeof arg_1 === 'string')
{
// If country code is supplied.
// `format('8005553535', 'RU', 'NATIONAL', [options], metadata)`.
if (typeof arg_3 === 'string')
{
format = arg_3
if (arg_5)
{
options = arg_4
metadata = arg_5
}
else
{
metadata = arg_4
}
input = parse(arg_1, { defaultCountry: arg_2, extended: true }, metadata)
}
// Just an international phone number is supplied
// `format('+78005553535', 'NATIONAL', [options], metadata)`.
else
{
if (typeof arg_2 !== 'string')
{
throw new Error('`format` argument not passed to `formatNumber(number, format)`')
}
format = arg_2
if (arg_4)
{
options = arg_3
metadata = arg_4
}
else
{
metadata = arg_3
}
input = parse(arg_1, { extended: true }, metadata)
}
}
// If the phone number is passed as a parsed number object.
// `format({ phone: '8005553535', country: 'RU' }, 'NATIONAL', [options], metadata)`.
else if (isObject(arg_1))
{
input = arg_1
format = arg_2
if (arg_4)
{
options = arg_3
metadata = arg_4
}
else
{
metadata = arg_3
}
}
else throw new TypeError('A phone number must either be a string or an object of shape { phone, [country] }.')
// Legacy lowercase formats.
if (format === 'International') {
format = 'INTERNATIONAL'
} else if (format === 'National') {
format = 'NATIONAL'
}
return {
input,
format,
options,
metadata
}
}

View File

@@ -0,0 +1,243 @@
import metadata from '../../metadata.min.json' with { type: 'json' }
import _formatNumber from './format.js'
function formatNumber(...parameters) {
parameters.push(metadata)
return _formatNumber.apply(this, parameters)
}
describe('format', () => {
it('should work with the first argument being a E.164 number', () => {
expect(formatNumber('+12133734253', 'NATIONAL')).to.equal('(213) 373-4253')
expect(formatNumber('+12133734253', 'INTERNATIONAL')).to.equal('+1 213 373 4253')
// Invalid number.
expect(formatNumber('+12111111111', 'NATIONAL')).to.equal('(211) 111-1111')
// Formatting invalid E.164 numbers.
expect(formatNumber('+11111', 'INTERNATIONAL')).to.equal('+1 1111')
expect(formatNumber('+11111', 'NATIONAL')).to.equal('1111')
})
it('should work with the first object argument expanded', () => {
expect(formatNumber('2133734253', 'US', 'NATIONAL')).to.equal('(213) 373-4253')
expect(formatNumber('2133734253', 'US', 'INTERNATIONAL')).to.equal('+1 213 373 4253')
})
it('should support legacy "National" / "International" formats', () => {
expect(formatNumber('2133734253', 'US', 'National')).to.equal('(213) 373-4253')
expect(formatNumber('2133734253', 'US', 'International')).to.equal('+1 213 373 4253')
})
it('should format using formats with no leading digits (`format.leadingDigitsPatterns().length === 0`)', () => {
expect(
formatNumber({ phone: '12345678901', countryCallingCode: 888 }, 'INTERNATIONAL')
).to.equal('+888 123 456 78901')
})
it('should sort out the arguments', () => {
const options = {
formatExtension: (number, extension) => `${number} доб. ${extension}`
}
expect(formatNumber({
phone : '8005553535',
country : 'RU',
ext : '123'
},
'NATIONAL', options)).to.equal('8 (800) 555-35-35 доб. 123')
// Parse number from string.
expect(formatNumber('+78005553535', 'NATIONAL', options)).to.equal('8 (800) 555-35-35')
expect(formatNumber('8005553535', 'RU', 'NATIONAL', options)).to.equal('8 (800) 555-35-35')
})
it('should format with national prefix when specifically instructed', () => {
// With national prefix.
expect(formatNumber('88005553535', 'RU', 'NATIONAL')).to.equal('8 (800) 555-35-35')
// Without national prefix via an explicitly set option.
expect(formatNumber('88005553535', 'RU', 'NATIONAL', { nationalPrefix: false })).to.equal('800 555-35-35')
})
it('should format valid phone numbers', () => {
// Switzerland
expect(formatNumber({ country: 'CH', phone: '446681800' }, 'INTERNATIONAL')).to.equal('+41 44 668 18 00')
expect(formatNumber({ country: 'CH', phone: '446681800' }, 'E.164')).to.equal('+41446681800')
expect(formatNumber({ country: 'CH', phone: '446681800' }, 'RFC3966')).to.equal('tel:+41446681800')
expect(formatNumber({ country: 'CH', phone: '446681800' }, 'NATIONAL')).to.equal('044 668 18 00')
// France
expect(formatNumber({ country: 'FR', phone: '169454850' }, 'NATIONAL')).to.equal('01 69 45 48 50')
// Kazakhstan
expect(formatNumber('+7 702 211 1111', 'NATIONAL')).to.equal('8 (702) 211 1111')
})
it('should format national numbers with national prefix even if it\'s optional', () => {
// Russia
expect(formatNumber({ country: 'RU', phone: '9991234567' }, 'NATIONAL')).to.equal('8 (999) 123-45-67')
})
it('should work in edge cases', () => {
let thrower
// No phone number
expect(formatNumber('', 'RU', 'INTERNATIONAL')).to.equal('')
expect(formatNumber('', 'RU', 'NATIONAL')).to.equal('')
expect(formatNumber({ country: 'RU', phone: '' }, 'INTERNATIONAL')).to.equal('+7')
expect(formatNumber({ country: 'RU', phone: '' }, 'NATIONAL')).to.equal('')
// No suitable format
expect(formatNumber('+121337342530', 'US', 'NATIONAL')).to.equal('21337342530')
// No suitable format (leading digits mismatch)
expect(formatNumber('28199999', 'AD', 'NATIONAL')).to.equal('28199999')
// Numerical `value`
thrower = () => formatNumber(89150000000, 'RU', 'NATIONAL')
expect(thrower).to.throw(
'A phone number must either be a string or an object of shape { phone, [country] }.'
)
// No metadata for country
expect(() => formatNumber('+121337342530', 'USA', 'NATIONAL')).to.throw('Unknown country')
expect(() => formatNumber('21337342530', 'USA', 'NATIONAL')).to.throw('Unknown country')
// No format type
thrower = () => formatNumber('+123')
expect(thrower).to.throw('`format` argument not passed')
// Unknown format type
thrower = () => formatNumber('123', 'US', 'Gay')
expect(thrower).to.throw('Unknown "format" argument')
// No metadata
thrower = () => _formatNumber('123', 'US', 'E.164')
expect(thrower).to.throw('`metadata`')
// No formats
expect(formatNumber('012345', 'AC', 'NATIONAL')).to.equal('012345')
// No `fromCountry` for `IDD` format.
expect(formatNumber('+78005553535', 'IDD')).to.be.undefined
// `fromCountry` has no default IDD prefix.
expect(formatNumber('+78005553535', 'IDD', { fromCountry: 'BO' })).to.be.undefined
// No such country.
expect(() => formatNumber({ phone: '123', country: 'USA' }, 'NATIONAL')).to.throw('Unknown country')
})
it('should format phone number extensions', () => {
// National
expect(formatNumber({
country: 'US',
phone: '2133734253',
ext: '123'
},
'NATIONAL')).to.equal('(213) 373-4253 ext. 123')
// International
expect(formatNumber({
country : 'US',
phone : '2133734253',
ext : '123'
},
'INTERNATIONAL')).to.equal('+1 213 373 4253 ext. 123')
// International
expect(formatNumber({
country : 'US',
phone : '2133734253',
ext : '123'
},
'INTERNATIONAL')).to.equal('+1 213 373 4253 ext. 123')
// E.164
expect(formatNumber({
country : 'US',
phone : '2133734253',
ext : '123'
},
'E.164')).to.equal('+12133734253')
// RFC3966
expect(formatNumber({
country : 'US',
phone : '2133734253',
ext : '123'
},
'RFC3966')).to.equal('tel:+12133734253;ext=123')
// Custom ext prefix.
expect(formatNumber({
country : 'GB',
phone : '7912345678',
ext : '123'
},
'INTERNATIONAL')).to.equal('+44 7912 345678 x123')
})
it('should work with Argentina numbers', () => {
// The same mobile number is written differently
// in different formats in Argentina:
// `9` gets prepended in international format.
expect(formatNumber({ country: 'AR', phone: '3435551212' }, 'INTERNATIONAL')).to.equal('+54 3435 55 1212')
expect(formatNumber({ country: 'AR', phone: '3435551212' }, 'NATIONAL')).to.equal('03435 55-1212')
})
it('should work with Mexico numbers', () => {
// Fixed line.
expect(formatNumber({ country: 'MX', phone: '4499780001' }, 'INTERNATIONAL')).to.equal('+52 449 978 0001')
expect(formatNumber({ country: 'MX', phone: '4499780001' }, 'NATIONAL')).to.equal('449 978 0001')
// or '(449)978-0001'.
// Mobile.
// `1` is prepended before area code to mobile numbers in international format.
expect(formatNumber({ country: 'MX', phone: '3312345678' }, 'INTERNATIONAL')).to.equal('+52 33 1234 5678')
expect(formatNumber({ country: 'MX', phone: '3312345678' }, 'NATIONAL')).to.equal('33 1234 5678')
// or '045 33 1234-5678'.
})
it('should format possible numbers', () => {
expect(formatNumber({ countryCallingCode: '7', phone: '1111111111' }, 'E.164')).to.equal('+71111111111')
expect(formatNumber({ countryCallingCode: '7', phone: '1111111111' }, 'NATIONAL')).to.equal('1111111111')
expect(
formatNumber({ countryCallingCode: '7', phone: '1111111111' }, 'INTERNATIONAL')
).to.equal('+7 1111111111')
})
it('should format IDD-prefixed number', () => {
// No `fromCountry`.
expect(formatNumber('+78005553535', 'IDD')).to.be.undefined
// No default IDD prefix.
expect(formatNumber('+78005553535', 'IDD', { fromCountry: 'BO' })).to.be.undefined
// Same country calling code.
expect(
formatNumber('+12133734253', 'IDD', { fromCountry: 'CA' })
).to.equal('1 (213) 373-4253')
expect(
formatNumber('+78005553535', 'IDD', { fromCountry: 'KZ' })
).to.equal('8 (800) 555-35-35')
// formatNumber('+78005553535', 'IDD', { fromCountry: 'US' }).should.equal('01178005553535')
expect(
formatNumber('+78005553535', 'IDD', { fromCountry: 'US' })
).to.equal('011 7 800 555 35 35')
})
it('should format non-geographic numbering plan phone numbers', () => {
// https://github.com/catamphetamine/libphonenumber-js/issues/323
expect(formatNumber('+870773111632', 'INTERNATIONAL')).to.equal('+870 773 111 632')
expect(formatNumber('+870773111632', 'NATIONAL')).to.equal('773 111 632')
})
it('should use the default IDD prefix when formatting a phone number', () => {
// Testing preferred international prefixes with ~ are supported.
// ("~" designates waiting on a line until proceeding with the input).
expect(formatNumber('+390236618300', 'IDD', { fromCountry: 'BY' })).to.equal('8~10 39 02 3661 8300')
})
})

View File

@@ -0,0 +1,121 @@
import isViablePhoneNumber from '../helpers/isViablePhoneNumber.js'
import _getNumberType from '../helpers/getNumberType.js'
import isObject from '../helpers/isObject.js'
import parse from '../parse.js'
// Finds out national phone number type (fixed line, mobile, etc)
export default function getNumberType() {
const { input, options, metadata } = normalizeArguments(arguments)
// `parseNumber()` would return `{}` when no phone number could be parsed from the input.
if (!input.phone) {
return
}
return _getNumberType(input, options, metadata)
}
// Sort out arguments
export function normalizeArguments(args)
{
// This line of code appeared to not work correctly with `babel`/`istanbul`:
// for some weird reason, it caused coverage less than 100%.
// That's because `babel`/`istanbul`, for some weird reason,
// apparently doesn't know how to properly exclude Babel polyfills from code coverage.
//
// const [arg_1, arg_2, arg_3, arg_4] = Array.prototype.slice.call(args)
const arg_1 = args[0]
const arg_2 = args[1]
const arg_3 = args[2]
const arg_4 = args[3]
let input
let options = {}
let metadata
// If the phone number is passed as a string.
// `getNumberType('88005553535', ...)`.
if (typeof arg_1 === 'string')
{
// If "default country" argument is being passed
// then convert it to an `options` object.
// `getNumberType('88005553535', 'RU', metadata)`.
if (!isObject(arg_2))
{
if (arg_4)
{
options = arg_3
metadata = arg_4
}
else
{
metadata = arg_3
}
// `parse` extracts phone numbers from raw text,
// therefore it will cut off all "garbage" characters,
// while this `validate` function needs to verify
// that the phone number contains no "garbage"
// therefore the explicit `isViablePhoneNumber` check.
if (isViablePhoneNumber(arg_1))
{
input = parse(arg_1, { defaultCountry: arg_2 }, metadata)
}
else
{
input = {}
}
}
// No "resrict country" argument is being passed.
// International phone number is passed.
// `getNumberType('+78005553535', metadata)`.
else
{
if (arg_3)
{
options = arg_2
metadata = arg_3
}
else
{
metadata = arg_2
}
// `parse` extracts phone numbers from raw text,
// therefore it will cut off all "garbage" characters,
// while this `validate` function needs to verify
// that the phone number contains no "garbage"
// therefore the explicit `isViablePhoneNumber` check.
if (isViablePhoneNumber(arg_1))
{
input = parse(arg_1, undefined, metadata)
}
else
{
input = {}
}
}
}
// If the phone number is passed as a parsed phone number.
// `getNumberType({ phone: '88005553535', country: 'RU' }, ...)`.
else if (isObject(arg_1))
{
input = arg_1
if (arg_3)
{
options = arg_2
metadata = arg_3
}
else
{
metadata = arg_2
}
}
else throw new TypeError('A phone number must either be a string or an object of shape { phone, [country] }.')
return {
input,
options,
metadata
}
}

View File

@@ -0,0 +1,67 @@
import metadata from '../../metadata.max.json' with { type: 'json' }
import Metadata from '../metadata.js'
import _getNumberType from './getNumberType.js'
function getNumberType(...parameters) {
parameters.push(metadata)
return _getNumberType.apply(this, parameters)
}
describe('getNumberType', () => {
it('should infer phone number type MOBILE', () => {
expect(getNumberType('9150000000', 'RU')).to.equal('MOBILE')
expect(getNumberType('7912345678', 'GB')).to.equal('MOBILE')
expect(getNumberType('51234567', 'EE')).to.equal('MOBILE')
})
it('should infer phone number types', () => {
expect(getNumberType('88005553535', 'RU')).to.equal('TOLL_FREE')
expect(getNumberType('8005553535', 'RU')).to.equal('TOLL_FREE')
expect(getNumberType('4957777777', 'RU')).to.equal('FIXED_LINE')
expect(getNumberType('8030000000', 'RU')).to.equal('PREMIUM_RATE')
expect(getNumberType('2133734253', 'US')).to.equal('FIXED_LINE_OR_MOBILE')
expect(getNumberType('5002345678', 'US')).to.equal('PERSONAL_NUMBER')
})
it('should work when no country is passed', () => {
expect(getNumberType('+79150000000')).to.equal('MOBILE')
})
it('should return FIXED_LINE_OR_MOBILE when there is ambiguity', () => {
// (no such country in the metadata, therefore no unit test for this `if`)
})
it('should work in edge cases', function() {
let thrower
// // No metadata
// thrower = () => _getNumberType({ phone: '+78005553535' })
// thrower.should.throw('`metadata` argument not passed')
// Parsed phone number
expect(getNumberType({ phone: '8005553535', country: 'RU' })).to.equal('TOLL_FREE')
// Invalid phone number
expect(type(getNumberType('123', 'RU'))).to.equal('undefined')
// Invalid country
thrower = () => getNumberType({ phone: '8005553535', country: 'RUS' })
expect(thrower).to.throw('Unknown country')
// Numerical `value`
thrower = () => getNumberType(89150000000, 'RU')
expect(thrower).to.throw(
'A phone number must either be a string or an object of shape { phone, [country] }.'
)
// When `options` argument is passed.
expect(getNumberType('8005553535', 'RU', {})).to.equal('TOLL_FREE')
expect(getNumberType('+78005553535', {})).to.equal('TOLL_FREE')
expect(getNumberType({ phone: '8005553535', country: 'RU' }, {})).to.equal('TOLL_FREE')
})
})
function type(something) {
return typeof something
}

View File

@@ -0,0 +1,25 @@
import { normalizeArguments } from './getNumberType.js'
import _isPossibleNumber from '../isPossible.js'
/**
* Checks if a given phone number is possible.
* Which means it only checks phone number length
* and doesn't test any regular expressions.
*
* Examples:
*
* ```js
* isPossibleNumber('+78005553535', metadata)
* isPossibleNumber('8005553535', 'RU', metadata)
* isPossibleNumber('88005553535', 'RU', metadata)
* isPossibleNumber({ phone: '8005553535', country: 'RU' }, metadata)
* ```
*/
export default function isPossibleNumber() {
const { input, options, metadata } = normalizeArguments(arguments)
// `parseNumber()` would return `{}` when no phone number could be parsed from the input.
if (!input.phone && !(options && options.v2)) {
return false
}
return _isPossibleNumber(input, options, metadata)
}

View File

@@ -0,0 +1,46 @@
import metadata from '../../metadata.min.json' with { type: 'json' }
import _isPossibleNumber from './isPossibleNumber.js'
function isPossibleNumber(...parameters) {
parameters.push(metadata)
return _isPossibleNumber.apply(this, parameters)
}
describe('isPossibleNumber', () => {
it('should work', function()
{
expect(isPossibleNumber('+79992223344')).to.equal(true)
expect(isPossibleNumber({ phone: '1112223344', country: 'RU' })).to.equal(true)
expect(isPossibleNumber({ phone: '111222334', country: 'RU' })).to.equal(false)
expect(isPossibleNumber({ phone: '11122233445', country: 'RU' })).to.equal(false)
expect(isPossibleNumber({ phone: '1112223344', countryCallingCode: 7 })).to.equal(true)
})
it('should work v2', () => {
expect(
isPossibleNumber({ nationalNumber: '111222334', countryCallingCode: 7 }, { v2: true })
).to.equal(false)
expect(
isPossibleNumber({ nationalNumber: '1112223344', countryCallingCode: 7 }, { v2: true })
).to.equal(true)
expect(
isPossibleNumber({ nationalNumber: '11122233445', countryCallingCode: 7 }, { v2: true })
).to.equal(false)
})
it('should work in edge cases', () => {
// Invalid `PhoneNumber` argument.
expect(() => isPossibleNumber({}, { v2: true })).to.throw('Invalid phone number object passed')
// Empty input is passed.
// This is just to support `isValidNumber({})`
// for cases when `parseNumber()` returns `{}`.
expect(isPossibleNumber({})).to.equal(false)
expect(() => isPossibleNumber({ phone: '1112223344' })).to.throw('Invalid phone number object passed')
// Incorrect country.
expect(() => isPossibleNumber({ phone: '1112223344', country: 'XX' })).to.throw('Unknown country')
})
})

View File

@@ -0,0 +1,12 @@
import _isValidNumber from '../isValid.js'
import { normalizeArguments } from './getNumberType.js'
// Finds out national phone number type (fixed line, mobile, etc)
export default function isValidNumber() {
const { input, options, metadata } = normalizeArguments(arguments)
// `parseNumber()` would return `{}` when no phone number could be parsed from the input.
if (!input.phone) {
return false
}
return _isValidNumber(input, options, metadata)
}

View File

@@ -0,0 +1,92 @@
import metadata from '../../metadata.min.json' with { type: 'json' }
import _isValidNumber from './isValidNumber.js'
function isValidNumber(...parameters) {
parameters.push(metadata)
return _isValidNumber.apply(this, parameters)
}
describe('isValidNumber', () => {
it('should validate phone numbers', () => {
expect(isValidNumber('+1-213-373-4253')).to.equal(true)
expect(isValidNumber('+1-213-373')).to.equal(false)
expect(isValidNumber('+1-213-373-4253', undefined)).to.equal(true)
expect(isValidNumber('(213) 373-4253', 'US')).to.equal(true)
expect(isValidNumber('(213) 37', 'US')).to.equal(false)
expect(isValidNumber({ country: 'US', phone: '2133734253' })).to.equal(true)
// No "types" info: should return `true`.
expect(isValidNumber('+380972423740')).to.equal(true)
expect(isValidNumber('0912345678', 'TW')).to.equal(true)
// Moible numbers starting 07624* are Isle of Man
// which has its own "country code" "IM"
// which is in the "GB" "country calling code" zone.
// So while this number is for "IM" it's still supposed to
// be valid when passed "GB" as a default country.
expect(isValidNumber('07624369230', 'GB')).to.equal(true)
})
it('should refine phone number validation in case extended regular expressions are set for a country', () => {
// Germany general validation must pass
expect(isValidNumber('961111111', 'UZ')).to.equal(true)
const phoneNumberTypePatterns = metadata.countries.UZ[11]
// Different regular expressions for precise national number validation.
// `types` index in compressed array is `9` for v1.
// For v2 it's 10.
// For v3 it's 11.
metadata.countries.UZ[11] =
[
["(?:6(?:1(?:22|3[124]|4[1-4]|5[123578]|64)|2(?:22|3[0-57-9]|41)|5(?:22|3[3-7]|5[024-8])|6\\d{2}|7(?:[23]\\d|7[69])|9(?:22|4[1-8]|6[135]))|7(?:0(?:5[4-9]|6[0146]|7[12456]|9[135-8])|1[12]\\d|2(?:22|3[1345789]|4[123579]|5[14])|3(?:2\\d|3[1578]|4[1-35-7]|5[1-57]|61)|4(?:2\\d|3[1-579]|7[1-79])|5(?:22|5[1-9]|6[1457])|6(?:22|3[12457]|4[13-8])|9(?:22|5[1-9])))\\d{5}"],
["6(?:1(?:2(?:98|2[01])|35[0-4]|50\\d|61[23]|7(?:[01][017]|4\\d|55|9[5-9]))|2(?:11\\d|2(?:[12]1|9[01379])|5(?:[126]\\d|3[0-4])|7\\d{2})|5(?:19[01]|2(?:27|9[26])|30\\d|59\\d|7\\d{2})|6(?:2(?:1[5-9]|2[0367]|38|41|52|60)|3[79]\\d|4(?:56|83)|7(?:[07]\\d|1[017]|3[07]|4[047]|5[057]|67|8[0178]|9[79])|9[0-3]\\d)|7(?:2(?:24|3[237]|4[5-9]|7[15-8])|5(?:7[12]|8[0589])|7(?:0\\d|[39][07])|9(?:0\\d|7[079]))|9(?:2(?:1[1267]|5\\d|3[01]|7[0-4])|5[67]\\d|6(?:2[0-26]|8\\d)|7\\d{2}))\\d{4}|7(?:0\\d{3}|1(?:13[01]|6(?:0[47]|1[67]|66)|71[3-69]|98\\d)|2(?:2(?:2[79]|95)|3(?:2[5-9]|6[0-6])|57\\d|7(?:0\\d|1[17]|2[27]|3[37]|44|5[057]|66|88))|3(?:2(?:1[0-6]|21|3[469]|7[159])|33\\d|5(?:0[0-4]|5[579]|9\\d)|7(?:[0-3579]\\d|4[0467]|6[67]|8[078])|9[4-6]\\d)|4(?:2(?:29|5[0257]|6[0-7]|7[1-57])|5(?:1[0-4]|8\\d|9[5-9])|7(?:0\\d|1[024589]|2[0127]|3[0137]|[46][07]|5[01]|7[5-9]|9[079])|9(?:7[015-9]|[89]\\d))|5(?:112|2(?:0\\d|2[29]|[49]4)|3[1568]\\d|52[6-9]|7(?:0[01578]|1[017]|[23]7|4[047]|[5-7]\\d|8[78]|9[079]))|6(?:2(?:2[1245]|4[2-4])|39\\d|41[179]|5(?:[349]\\d|5[0-2])|7(?:0[017]|[13]\\d|22|44|55|67|88))|9(?:22[128]|3(?:2[0-4]|7\\d)|57[05629]|7(?:2[05-9]|3[37]|4\\d|60|7[2579]|87|9[07])))\\d{4}|9[0-57-9]\\d{7}"]
]
// Extended validation must not pass for an invalid phone number
expect(isValidNumber('961111111', 'UZ')).to.equal(false)
// Extended validation must pass for a valid phone number
expect(isValidNumber('912345678', 'UZ')).to.equal(true)
metadata.countries.UZ[11] = phoneNumberTypePatterns
})
it('should work in edge cases', () => {
// No metadata
let thrower = () => _isValidNumber('+78005553535')
expect(thrower).to.throw('`metadata` argument not passed')
// Non-phone-number characters in a phone number
expect(isValidNumber('+499821958a')).to.equal(false)
expect(isValidNumber('88005553535x', 'RU')).to.equal(false)
// Doesn't have `types` regexps in default metadata.
expect(isValidNumber({ country: 'UA', phone: '300000000' })).to.equal(true)
expect(isValidNumber({ country: 'UA', phone: '200000000' })).to.equal(false)
// Numerical `value`
thrower = () => isValidNumber(88005553535, 'RU')
expect(thrower).to.throw(
'A phone number must either be a string or an object of shape { phone, [country] }.'
)
// Long country phone code
expect(isValidNumber('+3725555555')).to.equal(true)
// Invalid country
thrower = () => isValidNumber({ phone: '8005553535', country: 'RUS' })
expect(thrower).to.throw('Unknown country')
})
it('should accept phone number extensions', () => {
// International
expect(isValidNumber('+12133734253 ext. 123')).to.equal(true)
// National
expect(isValidNumber('88005553535 x123', 'RU')).to.equal(true)
})
})

View File

@@ -0,0 +1,62 @@
import isViablePhoneNumber from '../helpers/isViablePhoneNumber.js'
import parseNumber from '../parse.js'
import _isValidNumberForRegion from './isValidNumberForRegion_.js'
// This function has been deprecated and is not exported as
// `isValidPhoneNumberForCountry()` or `isValidPhoneNumberForRegion()`.
//
// The rationale is:
//
// * We don't use the "region" word, so "country" would be better.
//
// * It could be substituted with:
//
// ```js
// export default function isValidPhoneNumberForCountry(phoneNumberString, country) {
// const phoneNumber = parsePhoneNumber(phoneNumberString, {
// defaultCountry: country,
// // Demand that the entire input string must be a phone number.
// // Otherwise, it would "extract" a phone number from an input string.
// extract: false
// })
// if (!phoneNumber) {
// return false
// }
// if (phoneNumber.country !== country) {
// return false
// }
// return phoneNumber.isValid()
// }
// ```
//
// * Same function could be used for `isPossiblePhoneNumberForCountry()`
// by replacing `isValid()` with `isPossible()`.
//
// * The reason why this function is not exported is because its result is ambiguous.
// Suppose `false` is returned. It could mean any of:
// * Not a phone number.
// * The phone number is valid but belongs to another country or another calling code.
// * The phone number belongs to the correct country but is not valid digit-wise.
// All those three cases should be handled separately from a "User Experience" standpoint.
// Simply showing "Invalid phone number" error in all of those cases would be lazy UX.
export default function isValidNumberForRegion(number, country, metadata) {
if (typeof number !== 'string') {
throw new TypeError('number must be a string')
}
if (typeof country !== 'string') {
throw new TypeError('country must be a string')
}
// `parse` extracts phone numbers from raw text,
// therefore it will cut off all "garbage" characters,
// while this `validate` function needs to verify
// that the phone number contains no "garbage"
// therefore the explicit `isViablePhoneNumber` check.
let input
if (isViablePhoneNumber(number)) {
input = parseNumber(number, { defaultCountry: country }, metadata)
} else {
input = {}
}
return _isValidNumberForRegion(input, country, undefined, metadata)
}

View File

@@ -0,0 +1,28 @@
import metadata from '../../metadata.min.json' with { type: 'json' }
import isValidNumberForRegionCustom from './isValidNumberForRegion.js'
import _isValidNumberForRegion from './isValidNumberForRegion_.js'
function isValidNumberForRegion(...parameters) {
parameters.push(metadata)
return isValidNumberForRegionCustom.apply(this, parameters)
}
describe('isValidNumberForRegion', () => {
it('should detect if is valid number for region', () => {
expect(isValidNumberForRegion('07624369230', 'GB')).to.equal(false)
expect(isValidNumberForRegion('07624369230', 'IM')).to.equal(true)
})
it('should validate arguments', () => {
expect(() => isValidNumberForRegion({ phone: '7624369230', country: 'GB' })).to.throw('number must be a string')
expect(() => isValidNumberForRegion('7624369230')).to.throw('country must be a string')
})
it('should work in edge cases', () => {
// Not a "viable" phone number.
expect(isValidNumberForRegion('7', 'GB')).to.equal(false)
// `options` argument `if/else` coverage.
expect(_isValidNumberForRegion('07624369230', 'GB', {}, metadata)).to.equal(false)
})
})

View File

@@ -0,0 +1,13 @@
import isValidNumber from '../isValid.js'
/**
* Checks if a given phone number is valid within a given region.
* Is just an alias for `phoneNumber.isValid() && phoneNumber.country === country`.
* https://github.com/googlei18n/libphonenumber/blob/master/FAQ.md#when-should-i-use-isvalidnumberforregion
*/
export default function isValidNumberForRegion(input, country, options, metadata) {
// If assigning the `{}` default value is moved to the arguments above,
// code coverage would decrease for some weird reason.
options = options || {}
return input.country === country && isValidNumber(input, options, metadata)
}

View File

@@ -0,0 +1,7 @@
import _parseNumber from '../parse.js'
import normalizeArguments from '../normalizeArguments.js'
export default function parseNumber() {
const { text, options, metadata } = normalizeArguments(arguments)
return _parseNumber(text, options, metadata)
}

View File

@@ -0,0 +1,530 @@
import metadata from '../../metadata.min.json' with { type: 'json' }
import _parseNumber from './parse.js'
import Metadata from '../metadata.js'
function parseNumber(...parameters) {
parameters.push(metadata)
return _parseNumber.apply(this, parameters)
}
const USE_NON_GEOGRAPHIC_COUNTRY_CODE = false
describe('parse', () => {
it('should not parse invalid phone numbers', () => {
// Too short.
expect(parseNumber('+7 (800) 55-35-35')).to.deep.equal({})
// Too long.
expect(parseNumber('+7 (800) 55-35-35-55')).to.deep.equal({})
expect(parseNumber('+7 (800) 55-35-35', 'US')).to.deep.equal({})
expect(parseNumber('(800) 55 35 35', { defaultCountry: 'RU' })).to.deep.equal({})
expect(parseNumber('+1 187 215 5230', 'US')).to.deep.equal({})
expect(parseNumber('911231231', 'BE')).to.deep.equal({})
})
it('should parse valid phone numbers', () => {
// Instant loans
// https://www.youtube.com/watch?v=6e1pMrYH5jI
//
// Restrict to RU
expect(parseNumber('Phone: 8 (800) 555 35 35.', 'RU')).to.deep.equal({ country: 'RU', phone: '8005553535' })
// International format
expect(parseNumber('Phone: +7 (800) 555-35-35.')).to.deep.equal({ country: 'RU', phone: '8005553535' })
// // Restrict to US, but not a US country phone code supplied
// parseNumber('+7 (800) 555-35-35', 'US').should.deep.equal({})
// Restrict to RU
expect(parseNumber('(800) 555 35 35', 'RU')).to.deep.equal({ country: 'RU', phone: '8005553535' })
// Default to RU
expect(parseNumber('8 (800) 555 35 35', { defaultCountry: 'RU' })).to.deep.equal({ country: 'RU', phone: '8005553535' })
// Gangster partyline
expect(parseNumber('+1-213-373-4253')).to.deep.equal({ country: 'US', phone: '2133734253' })
// Switzerland (just in case)
expect(parseNumber('044 668 18 00', 'CH')).to.deep.equal({ country: 'CH', phone: '446681800' })
// China, Beijing
expect(parseNumber('010-852644821', 'CN')).to.deep.equal({ country: 'CN', phone: '10852644821' })
// France
expect(parseNumber('+33169454850')).to.deep.equal({ country: 'FR', phone: '169454850' })
// UK (Jersey)
expect(parseNumber('+44 7700 300000')).to.deep.equal({ country: 'JE', phone: '7700300000' })
// KZ
expect(parseNumber('+7 702 211 1111')).to.deep.equal({ country: 'KZ', phone: '7022111111' })
// Brazil
expect(parseNumber('11987654321', 'BR')).to.deep.equal({ country: 'BR', phone: '11987654321' })
// Long country phone code.
expect(parseNumber('+212659777777')).to.deep.equal({ country: 'MA', phone: '659777777' })
// No country could be derived.
// parseNumber('+212569887076').should.deep.equal({ countryPhoneCode: '212', phone: '569887076' })
// GB. Moible numbers starting 07624* are Isle of Man.
expect(parseNumber('07624369230', 'GB')).to.deep.equal({ country: 'IM', phone: '7624369230' })
})
it('should parse possible numbers', () => {
// Invalid phone number for a given country.
expect(parseNumber('1112223344', 'RU', { extended: true })).to.deep.equal({
country : 'RU',
countryCallingCode : '7',
phone : '1112223344',
carrierCode : undefined,
ext : undefined,
valid : false,
possible : true
})
// International phone number.
// Several countries with the same country phone code.
expect(parseNumber('+71112223344')).to.deep.equal({})
expect(parseNumber('+71112223344', { extended: true })).to.deep.equal({
country : undefined,
countryCallingCode : '7',
phone : '1112223344',
carrierCode : undefined,
ext : undefined,
valid : false,
possible : true
})
// International phone number.
// Single country with the given country phone code.
expect(parseNumber('+33011222333', { extended: true })).to.deep.equal({
country : 'FR',
countryCallingCode : '33',
phone : '011222333',
carrierCode : undefined,
ext : undefined,
valid : false,
possible : true
})
// Too short.
// Won't strip national prefix `8` because otherwise the number would be too short.
expect(parseNumber('+7 (800) 55-35-35', { extended: true })).to.deep.equal({
country : 'RU',
countryCallingCode : '7',
phone : '800553535',
carrierCode : undefined,
ext : undefined,
valid : false,
possible : false
})
// Too long.
expect(parseNumber('+1 213 37342530', { extended: true })).to.deep.equal({
country : undefined,
countryCallingCode : '1',
phone : '21337342530',
carrierCode : undefined,
ext : undefined,
valid : false,
possible : false
})
// No national number to be parsed.
expect(parseNumber('+996', { extended: true })).to.deep.equal({
// countryCallingCode : '996'
})
// Valid number.
expect(parseNumber('+78005553535', { extended: true })).to.deep.equal({
country : 'RU',
countryCallingCode : '7',
phone : '8005553535',
carrierCode : undefined,
ext : undefined,
valid : true,
possible : true
})
// https://github.com/catamphetamine/libphonenumber-js/issues/211
expect(parseNumber('+966', { extended: true })).to.deep.equal({})
expect(parseNumber('+9664', { extended: true })).to.deep.equal({})
expect(parseNumber('+96645', { extended: true })).to.deep.equal({
carrierCode : undefined,
phone : '45',
ext : undefined,
country : 'SA',
countryCallingCode : '966',
possible : false,
valid : false
})
})
it('should parse non-European digits', () => {
expect(parseNumber('+١٢١٢٢٣٢٣٢٣٢')).to.deep.equal({ country: 'US', phone: '2122323232' })
})
it('should work in edge cases', () => {
let thrower
// No input
expect(parseNumber('')).to.deep.equal({})
// No country phone code
expect(parseNumber('+')).to.deep.equal({})
// No country at all (non international number and no explicit country code)
expect(parseNumber('123')).to.deep.equal({})
// No country metadata for this `require` country code
thrower = () => parseNumber('123', 'ZZ')
expect(thrower).to.throw('Unknown country')
// No country metadata for this `default` country code
thrower = () => parseNumber('123', { defaultCountry: 'ZZ' })
expect(thrower).to.throw('Unknown country')
// Invalid country phone code
expect(parseNumber('+210')).to.deep.equal({})
// Invalid country phone code (extended parsing mode)
expect(parseNumber('+210', { extended: true })).to.deep.equal({})
// Too short of a number.
expect(parseNumber('1', 'US', { extended: true })).to.deep.equal({})
// Too long of a number.
expect(parseNumber('1111111111111111111', 'RU', { extended: true })).to.deep.equal({})
// Not a number.
expect(parseNumber('abcdefg', 'US', { extended: true })).to.deep.equal({})
// Country phone code beginning with a '0'
expect(parseNumber('+0123')).to.deep.equal({})
// Barbados NANPA phone number
expect(parseNumber('+12460000000')).to.deep.equal({ country: 'BB', phone: '2460000000' })
// // A case when country (restricted to) is not equal
// // to the one parsed out of an international number.
// parseNumber('+1-213-373-4253', 'RU').should.deep.equal({})
// National (significant) number too short
expect(parseNumber('2', 'US')).to.deep.equal({})
// National (significant) number too long
expect(parseNumber('222222222222222222', 'US')).to.deep.equal({})
// No `national_prefix_for_parsing`
expect(parseNumber('41111', 'AC')).to.deep.equal({ country: 'AC', phone: '41111'})
// https://github.com/catamphetamine/libphonenumber-js/issues/235
// `matchesEntirely()` bug fix.
expect(parseNumber('+4915784846111')).to.deep.equal({ country: 'DE', phone: '15784846111' })
// No metadata
thrower = () => _parseNumber('')
expect(thrower).to.throw('`metadata` argument not passed')
// Numerical `value`
thrower = () => parseNumber(2141111111, 'US')
expect(thrower).to.throw('A text for parsing must be a string.')
// Input string too long.
expect(
parseNumber('8005553535 ', 'RU')
).to.deep.equal({})
})
it('should parse phone number extensions', () => {
// "ext"
expect(parseNumber('2134567890 ext 123', 'US')).to.deep.equal({
country : 'US',
phone : '2134567890',
ext : '123'
})
// "ext."
expect(parseNumber('+12134567890 ext. 12345', 'US')).to.deep.equal({
country : 'US',
phone : '2134567890',
ext : '12345'
})
// "доб."
expect(parseNumber('+78005553535 доб. 1234', 'RU')).to.deep.equal({
country : 'RU',
phone : '8005553535',
ext : '1234'
})
// "#"
expect(parseNumber('+12134567890#1234')).to.deep.equal({
country : 'US',
phone : '2134567890',
ext : '1234'
})
// "x"
expect(parseNumber('+78005553535 x1234')).to.deep.equal({
country : 'RU',
phone : '8005553535',
ext : '1234'
})
// Not a valid extension
expect(parseNumber('2134567890 ext. abc', 'US')).to.deep.equal({
country : 'US',
phone : '2134567890'
})
})
it('should parse RFC 3966 phone numbers', () => {
expect(parseNumber('tel:+78005553535;ext=123')).to.deep.equal({
country : 'RU',
phone : '8005553535',
ext : '123'
})
// Should parse "visual separators".
expect(parseNumber('tel:+7(800)555-35.35;ext=123')).to.deep.equal({
country : 'RU',
phone : '8005553535',
ext : '123'
})
// Invalid number.
expect(parseNumber('tel:+7x8005553535;ext=123')).to.deep.equal({})
})
it('should parse invalid international numbers even if they are invalid', () => {
expect(parseNumber('+7(8)8005553535', 'RU')).to.deep.equal({
country : 'RU',
phone : '8005553535'
})
})
it('should parse carrier codes', () => {
expect(parseNumber('0 15 21 5555-5555', 'BR', { extended: true })).to.deep.equal({
country : 'BR',
countryCallingCode : '55',
phone : '2155555555',
carrierCode : '15',
ext : undefined,
valid : true,
possible : true
})
})
it('should parse IDD prefixes', () => {
expect(parseNumber('011 61 2 3456 7890', 'US')).to.deep.equal({
phone : '234567890',
country : 'AU'
})
expect(parseNumber('011 61 2 3456 7890', 'FR')).to.deep.equal({})
expect(parseNumber('00 61 2 3456 7890', 'US')).to.deep.equal({})
expect(parseNumber('810 61 2 3456 7890', 'RU')).to.deep.equal({
phone : '234567890',
country : 'AU'
})
})
it('should work with v2 API', () => {
parseNumber('+99989160151539')
})
it('should work with Argentina numbers', () => {
// The same mobile number is written differently
// in different formats in Argentina:
// `9` gets prepended in international format.
expect(parseNumber('+54 9 3435 55 1212')).to.deep.equal({
country: 'AR',
phone: '93435551212'
})
expect(parseNumber('0343 15-555-1212', 'AR')).to.deep.equal({
country: 'AR',
phone: '93435551212'
})
})
it('should work with Mexico numbers', () => {
// Fixed line.
expect(parseNumber('+52 449 978 0001')).to.deep.equal({
country: 'MX',
phone: '4499780001'
})
// "Dialling tokens 01, 02, 044, 045 and 1 are removed as they are
// no longer valid since August 2019."
//
// parseNumber('01 (449)978-0001', 'MX').should.deep.equal({
// country: 'MX',
// phone: '4499780001'
// })
expect(parseNumber('(449)978-0001', 'MX')).to.deep.equal({
country: 'MX',
phone: '4499780001'
})
// "Dialling tokens 01, 02, 044, 045 and 1 are removed as they are
// no longer valid since August 2019."
//
// // Mobile.
// // `1` is prepended before area code to mobile numbers in international format.
// parseNumber('+52 1 33 1234-5678', 'MX').should.deep.equal({
// country: 'MX',
// phone: '3312345678'
// })
expect(parseNumber('+52 33 1234-5678', 'MX')).to.deep.equal({
country: 'MX',
phone: '3312345678'
})
// "Dialling tokens 01, 02, 044, 045 and 1 are removed as they are
// no longer valid since August 2019."
//
// parseNumber('044 (33) 1234-5678', 'MX').should.deep.equal({
// country: 'MX',
// phone: '3312345678'
// })
// parseNumber('045 33 1234-5678', 'MX').should.deep.equal({
// country: 'MX',
// phone: '3312345678'
// })
})
it('should parse non-geographic numbering plan phone numbers', () => {
expect(parseNumber('+870773111632')).to.deep.equal(USE_NON_GEOGRAPHIC_COUNTRY_CODE ?
{
country: '001',
phone: '773111632'
} :
{})
})
it('should parse non-geographic numbering plan phone numbers (default country code)', () => {
expect(parseNumber('773111632', { defaultCallingCode: '870' })).to.deep.equal(USE_NON_GEOGRAPHIC_COUNTRY_CODE ?
{
country: '001',
phone: '773111632'
} :
{})
})
it('should parse non-geographic numbering plan phone numbers (extended)', () => {
expect(parseNumber('+870773111632', { extended: true })).to.deep.equal({
country: USE_NON_GEOGRAPHIC_COUNTRY_CODE ? '001' : undefined,
countryCallingCode: '870',
phone: '773111632',
carrierCode: undefined,
ext: undefined,
possible: true,
valid: true
})
})
it('should parse non-geographic numbering plan phone numbers (default country code) (extended)', () => {
expect(parseNumber('773111632', { defaultCallingCode: '870', extended: true })).to.deep.equal({
country: USE_NON_GEOGRAPHIC_COUNTRY_CODE ? '001' : undefined,
countryCallingCode: '870',
phone: '773111632',
carrierCode: undefined,
ext: undefined,
possible: true,
valid: true
})
})
it('shouldn\'t crash when invalid `defaultCallingCode` is passed', () => {
expect(() => parseNumber('773111632', { defaultCallingCode: '999' })).to.throw('Unknown calling code')
})
it('shouldn\'t set `country` when there\'s no `defaultCountry` and `defaultCallingCode` is not of a "non-geographic entity"', () => {
expect(parseNumber('88005553535', { defaultCallingCode: '7' })).to.deep.equal({
country: 'RU',
phone: '8005553535'
})
})
it('should correctly parse numbers starting with the same digit as the national prefix', () => {
// https://github.com/catamphetamine/libphonenumber-js/issues/373
// `BY`'s `national_prefix` is `8`.
expect(parseNumber('+37582004910060')).to.deep.equal({
country: 'BY',
phone: '82004910060'
});
})
it('should autocorrect numbers without a leading +', () => {
// https://github.com/catamphetamine/libphonenumber-js/issues/376
expect(parseNumber('375447521111', 'BY')).to.deep.equal({
country: 'BY',
phone: '447521111'
});
// https://github.com/catamphetamine/libphonenumber-js/issues/316
expect(parseNumber('33612902554', 'FR')).to.deep.equal({
country: 'FR',
phone: '612902554'
});
// https://github.com/catamphetamine/libphonenumber-js/issues/375
expect(parseNumber('61438331999', 'AU')).to.deep.equal({
country: 'AU',
phone: '438331999'
});
// A case when `49` is a country calling code of a number without a leading `+`.
expect(parseNumber('4930123456', 'DE')).to.deep.equal({
country: 'DE',
phone: '30123456'
});
// A case when `49` is a valid area code.
expect(parseNumber('4951234567890', 'DE')).to.deep.equal({
country: 'DE',
phone: '4951234567890'
});
})
it('should parse extensions (long extensions with explicitl abels)', () => {
// Test lower and upper limits of extension lengths for each type of label.
// Firstly, when in RFC format: PhoneNumberUtil.extLimitAfterExplicitLabel
expect(parseNumber('33316005 ext 0', 'NZ').ext).to.equal('0')
expect(parseNumber('33316005 ext 01234567890123456789', 'NZ').ext).to.equal('01234567890123456789')
// Extension too long.
expect(parseNumber('33316005 ext 012345678901234567890', 'NZ').ext).to.be.undefined
// Explicit extension label.
expect(parseNumber('03 3316005ext:1', 'NZ').ext).to.equal('1')
expect(parseNumber('03 3316005 xtn:12345678901234567890', 'NZ').ext).to.equal('12345678901234567890')
expect(parseNumber('03 3316005 extension\t12345678901234567890', 'NZ').ext).to.equal('12345678901234567890')
expect(parseNumber('03 3316005 xtensio:12345678901234567890', 'NZ').ext).to.equal('12345678901234567890')
expect(parseNumber('03 3316005 xtensión, 12345678901234567890#', 'NZ').ext).to.equal('12345678901234567890')
expect(parseNumber('03 3316005extension.12345678901234567890', 'NZ').ext).to.equal('12345678901234567890')
expect(parseNumber('03 3316005 доб:12345678901234567890', 'NZ').ext).to.equal('12345678901234567890')
// Extension too long.
expect(parseNumber('03 3316005 extension 123456789012345678901', 'NZ').ext).to.be.undefined
})
it('should parse extensions (long extensions with auto dialling labels)', () => {
expect(parseNumber('+12679000000,,123456789012345#').ext).to.equal('123456789012345')
expect(parseNumber('+12679000000;123456789012345#').ext).to.equal('123456789012345')
expect(parseNumber('+442034000000,,123456789#').ext).to.equal('123456789')
// Extension too long.
expect(parseNumber('+12679000000,,1234567890123456#').ext).to.be.undefined
})
it('should parse extensions (short extensions with ambiguous characters)', () => {
expect(parseNumber('03 3316005 x 123456789', 'NZ').ext).to.equal('123456789')
expect(parseNumber('03 3316005 x. 123456789', 'NZ').ext).to.equal('123456789')
expect(parseNumber('03 3316005 #123456789#', 'NZ').ext).to.equal('123456789')
expect(parseNumber('03 3316005 ~ 123456789', 'NZ').ext).to.equal('123456789')
// Extension too long.
expect(parseNumber('03 3316005 ~ 1234567890', 'NZ').ext).to.be.undefined
})
it('should parse extensions (short extensions when not sure of label)', () => {
expect(parseNumber('+1123-456-7890 666666#', { v2: true }).ext).to.equal('666666')
expect(parseNumber('+11234567890-6#', { v2: true }).ext).to.equal('6')
// Extension too long.
expect(() => parseNumber('+1123-456-7890 7777777#', { v2: true })).to.throw('NOT_A_NUMBER')
})
})

View File

@@ -0,0 +1,30 @@
import normalizeArguments from '../normalizeArguments.js'
import PhoneNumberMatcher from '../PhoneNumberMatcher.js'
/**
* @return ES6 `for ... of` iterator.
*/
export default function searchNumbers()
{
const { text, options, metadata } = normalizeArguments(arguments)
const matcher = new PhoneNumberMatcher(text, options, metadata)
return {
[Symbol.iterator]() {
return {
next: () => {
if (matcher.hasNext()) {
return {
done: false,
value: matcher.next()
}
}
return {
done: true
}
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
import searchNumbers from './searchNumbers.js'
import metadata from '../../metadata.min.json' with { type: 'json' }
describe('searchNumbers', () => {
it('should iterate', () => {
const expectedNumbers =[{
country : 'RU',
phone : '8005553535',
// number : '+7 (800) 555-35-35',
startsAt : 14,
endsAt : 32
}, {
country : 'US',
phone : '2133734253',
// number : '(213) 373-4253',
startsAt : 41,
endsAt : 55
}]
for (const number of searchNumbers('The number is +7 (800) 555-35-35 and not (213) 373-4253 as written in the document.', 'US', metadata)) {
expect(number).to.deep.equal(expectedNumbers.shift())
}
expect(expectedNumbers.length).to.equal(0)
})
})