15815213711
2024-08-26 67b8b6731811983447e053d4396b3708c14dfe3c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// This is a legacy function.
// Use `findNumbers()` instead.
 
import {
    PLUS_CHARS,
    VALID_PUNCTUATION,
    VALID_DIGITS,
    WHITESPACE
} from '../constants.js'
 
import parse from '../parse.js'
import { VALID_PHONE_NUMBER_WITH_EXTENSION } from '../helpers/isViablePhoneNumber.js'
import createExtensionPattern from '../helpers/extension/createExtensionPattern.js'
 
import parsePreCandidate from '../findNumbers/parsePreCandidate.js'
import isValidPreCandidate from '../findNumbers/isValidPreCandidate.js'
import isValidCandidate from '../findNumbers/isValidCandidate.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')
 
const WHITESPACE_IN_THE_BEGINNING_PATTERN = new RegExp('^[' + WHITESPACE + ']+')
const PUNCTUATION_IN_THE_END_PATTERN = new RegExp('[' + VALID_PUNCTUATION + ']+$')
 
// // 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)
    return  {
        [Symbol.iterator]() {
            return {
                next: () => {
                    if (search.hasNext()) {
                        return {
                            done: false,
                            value: search.next()
                        }
                    }
                    return {
                        done: true
                    }
                }
            }
        }
    }
}
 
/**
 * Extracts a parseable phone number including any opening brackets, etc.
 * @param  {string} text - Input.
 * @return {object} `{ ?number, ?startsAt, ?endsAt }`.
 */
export 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
    }
}