1 | import extractCountryCallingCode from './helpers/extractCountryCallingCode'
|
2 | import extractCountryCallingCodeFromInternationalNumberWithoutPlusSign from './helpers/extractCountryCallingCodeFromInternationalNumberWithoutPlusSign'
|
3 | import extractNationalNumberFromPossiblyIncompleteNumber from './helpers/extractNationalNumberFromPossiblyIncompleteNumber'
|
4 | import stripIddPrefix from './helpers/stripIddPrefix'
|
5 | import parseDigits from './helpers/parseDigits'
|
6 |
|
7 | import {
|
8 | VALID_DIGITS,
|
9 | VALID_PUNCTUATION,
|
10 | PLUS_CHARS
|
11 | } from './constants'
|
12 |
|
13 | const VALID_FORMATTED_PHONE_NUMBER_PART =
|
14 | '[' +
|
15 | VALID_PUNCTUATION +
|
16 | VALID_DIGITS +
|
17 | ']+'
|
18 |
|
19 | const VALID_FORMATTED_PHONE_NUMBER_PART_PATTERN = new RegExp('^' + VALID_FORMATTED_PHONE_NUMBER_PART + '$', 'i')
|
20 |
|
21 | const VALID_PHONE_NUMBER =
|
22 | '(?:' +
|
23 | '[' + PLUS_CHARS + ']' +
|
24 | '[' +
|
25 | VALID_PUNCTUATION +
|
26 | VALID_DIGITS +
|
27 | ']*' +
|
28 | '|' +
|
29 | '[' +
|
30 | VALID_PUNCTUATION +
|
31 | VALID_DIGITS +
|
32 | ']+' +
|
33 | ')'
|
34 |
|
35 | const AFTER_PHONE_NUMBER_DIGITS_END_PATTERN = new RegExp(
|
36 | '[^' +
|
37 | VALID_PUNCTUATION +
|
38 | VALID_DIGITS +
|
39 | ']+' +
|
40 | '.*' +
|
41 | '$'
|
42 | )
|
43 |
|
44 | // Tests whether `national_prefix_for_parsing` could match
|
45 | // different national prefixes.
|
46 | // Matches anything that's not a digit or a square bracket.
|
47 | const COMPLEX_NATIONAL_PREFIX = /[^\d\[\]]/
|
48 |
|
49 | export default class AsYouTypeParser {
|
50 | constructor({
|
51 | defaultCountry,
|
52 | defaultCallingCode,
|
53 | metadata,
|
54 | onNationalSignificantNumberChange
|
55 | }) {
|
56 | this.defaultCountry = defaultCountry
|
57 | this.defaultCallingCode = defaultCallingCode
|
58 | this.metadata = metadata
|
59 | this.onNationalSignificantNumberChange = onNationalSignificantNumberChange
|
60 | }
|
61 |
|
62 | input(text, state) {
|
63 | const [formattedDigits, hasPlus] = extractFormattedDigitsAndPlus(text)
|
64 | const digits = parseDigits(formattedDigits)
|
65 | // Checks for a special case: just a leading `+` has been entered.
|
66 | let justLeadingPlus
|
67 | if (hasPlus) {
|
68 | if (!state.digits) {
|
69 | state.startInternationalNumber()
|
70 | if (!digits) {
|
71 | justLeadingPlus = true
|
72 | }
|
73 | }
|
74 | }
|
75 | if (digits) {
|
76 | this.inputDigits(digits, state)
|
77 | }
|
78 | return {
|
79 | digits,
|
80 | justLeadingPlus
|
81 | }
|
82 | }
|
83 |
|
84 | /**
|
85 | * Inputs "next" phone number digits.
|
86 | * @param {string} digits
|
87 | * @return {string} [formattedNumber] Formatted national phone number (if it can be formatted at this stage). Returning `undefined` means "don't format the national phone number at this stage".
|
88 | */
|
89 | inputDigits(nextDigits, state) {
|
90 | const { digits } = state
|
91 | const hasReceivedThreeLeadingDigits = digits.length < 3 && digits.length + nextDigits.length >= 3
|
92 |
|
93 | // Append phone number digits.
|
94 | state.appendDigits(nextDigits)
|
95 |
|
96 | // Attempt to extract IDD prefix:
|
97 | // Some users input their phone number in international format,
|
98 | // but in an "out-of-country" dialing format instead of using the leading `+`.
|
99 | // https://github.com/catamphetamine/libphonenumber-js/issues/185
|
100 | // Detect such numbers as soon as there're at least 3 digits.
|
101 | // Google's library attempts to extract IDD prefix at 3 digits,
|
102 | // so this library just copies that behavior.
|
103 | // I guess that's because the most commot IDD prefixes are
|
104 | // `00` (Europe) and `011` (US).
|
105 | // There exist really long IDD prefixes too:
|
106 | // for example, in Australia the default IDD prefix is `0011`,
|
107 | // and it could even be as long as `14880011`.
|
108 | // An IDD prefix is extracted here, and then every time when
|
109 | // there's a new digit and the number couldn't be formatted.
|
110 | if (hasReceivedThreeLeadingDigits) {
|
111 | this.extractIddPrefix(state)
|
112 | }
|
113 |
|
114 | if (this.isWaitingForCountryCallingCode(state)) {
|
115 | if (!this.extractCountryCallingCode(state)) {
|
116 | return
|
117 | }
|
118 | } else {
|
119 | state.appendNationalSignificantNumberDigits(nextDigits)
|
120 | }
|
121 |
|
122 | // If a phone number is being input in international format,
|
123 | // then it's not valid for it to have a national prefix.
|
124 | // Still, some people incorrectly input such numbers with a national prefix.
|
125 | // In such cases, only attempt to strip a national prefix if the number becomes too long.
|
126 | // (but that is done later, not here)
|
127 | if (!state.international) {
|
128 | if (!this.hasExtractedNationalSignificantNumber) {
|
129 | this.extractNationalSignificantNumber(state.getNationalDigits(), state.update)
|
130 | }
|
131 | }
|
132 | }
|
133 |
|
134 | isWaitingForCountryCallingCode({ international, callingCode }) {
|
135 | return international && !callingCode
|
136 | }
|
137 |
|
138 | // Extracts a country calling code from a number
|
139 | // being entered in internatonal format.
|
140 | extractCountryCallingCode(state) {
|
141 | const { countryCallingCode, number } = extractCountryCallingCode(
|
142 | '+' + state.getDigitsWithoutInternationalPrefix(),
|
143 | this.defaultCountry,
|
144 | this.defaultCallingCode,
|
145 | this.metadata.metadata
|
146 | )
|
147 | if (countryCallingCode) {
|
148 | state.setCallingCode(countryCallingCode)
|
149 | state.update({
|
150 | nationalSignificantNumber: number
|
151 | })
|
152 | return true
|
153 | }
|
154 | }
|
155 |
|
156 | reset(numberingPlan) {
|
157 | if (numberingPlan) {
|
158 | this.hasSelectedNumberingPlan = true
|
159 | const nationalPrefixForParsing = numberingPlan._nationalPrefixForParsing()
|
160 | this.couldPossiblyExtractAnotherNationalSignificantNumber = nationalPrefixForParsing && COMPLEX_NATIONAL_PREFIX.test(nationalPrefixForParsing)
|
161 | } else {
|
162 | this.hasSelectedNumberingPlan = undefined
|
163 | this.couldPossiblyExtractAnotherNationalSignificantNumber = undefined
|
164 | }
|
165 | }
|
166 |
|
167 | /**
|
168 | * Extracts a national (significant) number from user input.
|
169 | * Google's library is different in that it only applies `national_prefix_for_parsing`
|
170 | * and doesn't apply `national_prefix_transform_rule` after that.
|
171 | * https://github.com/google/libphonenumber/blob/a3d70b0487875475e6ad659af404943211d26456/java/libphonenumber/src/com/google/i18n/phonenumbers/AsYouTypeFormatter.java#L539
|
172 | * @return {boolean} [extracted]
|
173 | */
|
174 | extractNationalSignificantNumber(nationalDigits, setState) {
|
175 | if (!this.hasSelectedNumberingPlan) {
|
176 | return
|
177 | }
|
178 | const {
|
179 | nationalPrefix,
|
180 | nationalNumber,
|
181 | carrierCode
|
182 | } = extractNationalNumberFromPossiblyIncompleteNumber(
|
183 | nationalDigits,
|
184 | this.metadata
|
185 | )
|
186 | if (nationalNumber === nationalDigits) {
|
187 | return
|
188 | }
|
189 | this.onExtractedNationalNumber(
|
190 | nationalPrefix,
|
191 | carrierCode,
|
192 | nationalNumber,
|
193 | nationalDigits,
|
194 | setState
|
195 | )
|
196 | return true
|
197 | }
|
198 |
|
199 | /**
|
200 | * In Google's code this function is called "attempt to extract longer NDD".
|
201 | * "Some national prefixes are a substring of others", they say.
|
202 | * @return {boolean} [result] — Returns `true` if extracting a national prefix produced different results from what they were.
|
203 | */
|
204 | extractAnotherNationalSignificantNumber(nationalDigits, prevNationalSignificantNumber, setState) {
|
205 | if (!this.hasExtractedNationalSignificantNumber) {
|
206 | return this.extractNationalSignificantNumber(nationalDigits, setState)
|
207 | }
|
208 | if (!this.couldPossiblyExtractAnotherNationalSignificantNumber) {
|
209 | return
|
210 | }
|
211 | const {
|
212 | nationalPrefix,
|
213 | nationalNumber,
|
214 | carrierCode
|
215 | } = extractNationalNumberFromPossiblyIncompleteNumber(
|
216 | nationalDigits,
|
217 | this.metadata
|
218 | )
|
219 | // If a national prefix has been extracted previously,
|
220 | // then it's always extracted as additional digits are added.
|
221 | // That's assuming `extractNationalNumberFromPossiblyIncompleteNumber()`
|
222 | // doesn't do anything different from what it currently does.
|
223 | // So, just in case, here's this check, though it doesn't occur.
|
224 | /* istanbul ignore if */
|
225 | if (nationalNumber === prevNationalSignificantNumber) {
|
226 | return
|
227 | }
|
228 | this.onExtractedNationalNumber(
|
229 | nationalPrefix,
|
230 | carrierCode,
|
231 | nationalNumber,
|
232 | nationalDigits,
|
233 | setState
|
234 | )
|
235 | return true
|
236 | }
|
237 |
|
238 | onExtractedNationalNumber(
|
239 | nationalPrefix,
|
240 | carrierCode,
|
241 | nationalSignificantNumber,
|
242 | nationalDigits,
|
243 | setState
|
244 | ) {
|
245 | let complexPrefixBeforeNationalSignificantNumber
|
246 | let nationalSignificantNumberMatchesInput
|
247 | // This check also works with empty `this.nationalSignificantNumber`.
|
248 | const nationalSignificantNumberIndex = nationalDigits.lastIndexOf(nationalSignificantNumber)
|
249 | // If the extracted national (significant) number is the
|
250 | // last substring of the `digits`, then it means that it hasn't been altered:
|
251 | // no digits have been removed from the national (significant) number
|
252 | // while applying `national_prefix_transform_rule`.
|
253 | // https://gitlab.com/catamphetamine/libphonenumber-js/-/blob/master/METADATA.md#national_prefix_for_parsing--national_prefix_transform_rule
|
254 | if (nationalSignificantNumberIndex >= 0 &&
|
255 | nationalSignificantNumberIndex === nationalDigits.length - nationalSignificantNumber.length) {
|
256 | nationalSignificantNumberMatchesInput = true
|
257 | // If a prefix of a national (significant) number is not as simple
|
258 | // as just a basic national prefix, then such prefix is stored in
|
259 | // `this.complexPrefixBeforeNationalSignificantNumber` property and will be
|
260 | // prepended "as is" to the national (significant) number to produce
|
261 | // a formatted result.
|
262 | const prefixBeforeNationalNumber = nationalDigits.slice(0, nationalSignificantNumberIndex)
|
263 | // `prefixBeforeNationalNumber` is always non-empty,
|
264 | // because `onExtractedNationalNumber()` isn't called
|
265 | // when a national (significant) number hasn't been actually "extracted":
|
266 | // when a national (significant) number is equal to the national part of `digits`,
|
267 | // then `onExtractedNationalNumber()` doesn't get called.
|
268 | if (prefixBeforeNationalNumber !== nationalPrefix) {
|
269 | complexPrefixBeforeNationalSignificantNumber = prefixBeforeNationalNumber
|
270 | }
|
271 | }
|
272 | setState({
|
273 | nationalPrefix,
|
274 | carrierCode,
|
275 | nationalSignificantNumber,
|
276 | nationalSignificantNumberMatchesInput,
|
277 | complexPrefixBeforeNationalSignificantNumber
|
278 | })
|
279 | // `onExtractedNationalNumber()` is only called when
|
280 | // the national (significant) number actually did change.
|
281 | this.hasExtractedNationalSignificantNumber = true
|
282 | this.onNationalSignificantNumberChange()
|
283 | }
|
284 |
|
285 | reExtractNationalSignificantNumber(state) {
|
286 | // Attempt to extract a national prefix.
|
287 | //
|
288 | // Some people incorrectly input national prefix
|
289 | // in an international phone number.
|
290 | // For example, some people write British phone numbers as `+44(0)...`.
|
291 | //
|
292 | // Also, in some rare cases, it is valid for a national prefix
|
293 | // to be a part of an international phone number.
|
294 | // For example, mobile phone numbers in Mexico are supposed to be
|
295 | // dialled internationally using a `1` national prefix,
|
296 | // so the national prefix will be part of an international number.
|
297 | //
|
298 | // Quote from:
|
299 | // https://www.mexperience.com/dialing-cell-phones-in-mexico/
|
300 | //
|
301 | // "Dialing a Mexican cell phone from abroad
|
302 | // When you are calling a cell phone number in Mexico from outside Mexico,
|
303 | // it’s necessary to dial an additional “1” after Mexico’s country code
|
304 | // (which is “52”) and before the area code.
|
305 | // You also ignore the 045, and simply dial the area code and the
|
306 | // cell phone’s number.
|
307 | //
|
308 | // If you don’t add the “1”, you’ll receive a recorded announcement
|
309 | // asking you to redial using it.
|
310 | //
|
311 | // For example, if you are calling from the USA to a cell phone
|
312 | // in Mexico City, you would dial +52 – 1 – 55 – 1234 5678.
|
313 | // (Note that this is different to calling a land line in Mexico City
|
314 | // from abroad, where the number dialed would be +52 – 55 – 1234 5678)".
|
315 | //
|
316 | // Google's demo output:
|
317 | // https://libphonenumber.appspot.com/phonenumberparser?number=%2b5215512345678&country=MX
|
318 | //
|
319 | if (this.extractAnotherNationalSignificantNumber(
|
320 | state.getNationalDigits(),
|
321 | state.nationalSignificantNumber,
|
322 | state.update
|
323 | )) {
|
324 | return true
|
325 | }
|
326 | // If no format matches the phone number, then it could be
|
327 | // "a really long IDD" (quote from a comment in Google's library).
|
328 | // An IDD prefix is first extracted when the user has entered at least 3 digits,
|
329 | // and then here — every time when there's a new digit and the number
|
330 | // couldn't be formatted.
|
331 | // For example, in Australia the default IDD prefix is `0011`,
|
332 | // and it could even be as long as `14880011`.
|
333 | //
|
334 | // Could also check `!hasReceivedThreeLeadingDigits` here
|
335 | // to filter out the case when this check duplicates the one
|
336 | // already performed when there're 3 leading digits,
|
337 | // but it's not a big deal, and in most cases there
|
338 | // will be a suitable `format` when there're 3 leading digits.
|
339 | //
|
340 | if (this.extractIddPrefix(state)) {
|
341 | this.extractCallingCodeAndNationalSignificantNumber(state)
|
342 | return true
|
343 | }
|
344 | // Google's AsYouType formatter supports sort of an "autocorrection" feature
|
345 | // when it "autocorrects" numbers that have been input for a country
|
346 | // with that country's calling code.
|
347 | // Such "autocorrection" feature looks weird, but different people have been requesting it:
|
348 | // https://github.com/catamphetamine/libphonenumber-js/issues/376
|
349 | // https://github.com/catamphetamine/libphonenumber-js/issues/375
|
350 | // https://github.com/catamphetamine/libphonenumber-js/issues/316
|
351 | if (this.fixMissingPlus(state)) {
|
352 | this.extractCallingCodeAndNationalSignificantNumber(state)
|
353 | return true
|
354 | }
|
355 | }
|
356 |
|
357 | extractIddPrefix(state) {
|
358 | // An IDD prefix can't be present in a number written with a `+`.
|
359 | // Also, don't re-extract an IDD prefix if has already been extracted.
|
360 | const {
|
361 | international,
|
362 | IDDPrefix,
|
363 | digits,
|
364 | nationalSignificantNumber
|
365 | } = state
|
366 | if (international || IDDPrefix) {
|
367 | return
|
368 | }
|
369 | // Some users input their phone number in "out-of-country"
|
370 | // dialing format instead of using the leading `+`.
|
371 | // https://github.com/catamphetamine/libphonenumber-js/issues/185
|
372 | // Detect such numbers.
|
373 | const numberWithoutIDD = stripIddPrefix(
|
374 | digits,
|
375 | this.defaultCountry,
|
376 | this.defaultCallingCode,
|
377 | this.metadata.metadata
|
378 | )
|
379 | if (numberWithoutIDD !== undefined && numberWithoutIDD !== digits) {
|
380 | // If an IDD prefix was stripped then convert the IDD-prefixed number
|
381 | // to international number for subsequent parsing.
|
382 | state.update({
|
383 | IDDPrefix: digits.slice(0, digits.length - numberWithoutIDD.length)
|
384 | })
|
385 | this.startInternationalNumber(state)
|
386 | return true
|
387 | }
|
388 | }
|
389 |
|
390 | fixMissingPlus(state) {
|
391 | if (!state.international) {
|
392 | const {
|
393 | countryCallingCode: newCallingCode,
|
394 | number
|
395 | } = extractCountryCallingCodeFromInternationalNumberWithoutPlusSign(
|
396 | state.digits,
|
397 | this.defaultCountry,
|
398 | this.defaultCallingCode,
|
399 | this.metadata.metadata
|
400 | )
|
401 | if (newCallingCode) {
|
402 | state.update({
|
403 | missingPlus: true
|
404 | })
|
405 | this.startInternationalNumber(state)
|
406 | return true
|
407 | }
|
408 | }
|
409 | }
|
410 |
|
411 | startInternationalNumber(state) {
|
412 | state.startInternationalNumber()
|
413 | // If a national (significant) number has been extracted before, reset it.
|
414 | if (state.nationalSignificantNumber) {
|
415 | state.resetNationalSignificantNumber()
|
416 | this.onNationalSignificantNumberChange()
|
417 | this.hasExtractedNationalSignificantNumber = undefined
|
418 | }
|
419 | }
|
420 |
|
421 | extractCallingCodeAndNationalSignificantNumber(state) {
|
422 | if (this.extractCountryCallingCode(state)) {
|
423 | // `this.extractCallingCode()` is currently called when the number
|
424 | // couldn't be formatted during the standard procedure.
|
425 | // Normally, the national prefix would be re-extracted
|
426 | // for an international number if such number couldn't be formatted,
|
427 | // but since it's already not able to be formatted,
|
428 | // there won't be yet another retry, so also extract national prefix here.
|
429 | this.extractNationalSignificantNumber(state.getNationalDigits(), state.update)
|
430 | }
|
431 | }
|
432 | }
|
433 |
|
434 | /**
|
435 | * Extracts formatted phone number from text (if there's any).
|
436 | * @param {string} text
|
437 | * @return {string} [formattedPhoneNumber]
|
438 | */
|
439 | function extractFormattedPhoneNumber(text) {
|
440 | // Attempt to extract a possible number from the string passed in.
|
441 | const startsAt = text.search(VALID_PHONE_NUMBER)
|
442 | if (startsAt < 0) {
|
443 | return
|
444 | }
|
445 | // Trim everything to the left of the phone number.
|
446 | text = text.slice(startsAt)
|
447 | // Trim the `+`.
|
448 | let hasPlus
|
449 | if (text[0] === '+') {
|
450 | hasPlus = true
|
451 | text = text.slice('+'.length)
|
452 | }
|
453 | // Trim everything to the right of the phone number.
|
454 | text = text.replace(AFTER_PHONE_NUMBER_DIGITS_END_PATTERN, '')
|
455 | // Re-add the previously trimmed `+`.
|
456 | if (hasPlus) {
|
457 | text = '+' + text
|
458 | }
|
459 | return text
|
460 | }
|
461 |
|
462 | /**
|
463 | * Extracts formatted phone number digits (and a `+`) from text (if there're any).
|
464 | * @param {string} text
|
465 | * @return {any[]}
|
466 | */
|
467 | function _extractFormattedDigitsAndPlus(text) {
|
468 | // Extract a formatted phone number part from text.
|
469 | const extractedNumber = extractFormattedPhoneNumber(text) || ''
|
470 | // Trim a `+`.
|
471 | if (extractedNumber[0] === '+') {
|
472 | return [extractedNumber.slice('+'.length), true]
|
473 | }
|
474 | return [extractedNumber]
|
475 | }
|
476 |
|
477 | /**
|
478 | * Extracts formatted phone number digits (and a `+`) from text (if there're any).
|
479 | * @param {string} text
|
480 | * @return {any[]}
|
481 | */
|
482 | export function extractFormattedDigitsAndPlus(text) {
|
483 | let [formattedDigits, hasPlus] = _extractFormattedDigitsAndPlus(text)
|
484 | // If the extracted phone number part
|
485 | // can possibly be a part of some valid phone number
|
486 | // then parse phone number characters from a formatted phone number.
|
487 | if (!VALID_FORMATTED_PHONE_NUMBER_PART_PATTERN.test(formattedDigits)) {
|
488 | formattedDigits = ''
|
489 | }
|
490 | return [formattedDigits, hasPlus]
|
491 | } |
\ | No newline at end of file |