@whiteslove/parsing-lexicon 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +137 -137
- package/index.d.ts +374 -374
- package/package.json +77 -75
- package/src/central-asia-locations.js +211 -211
- package/src/central-asia.js +233 -233
- package/src/contact.d.ts +44 -44
- package/src/contact.js +159 -159
- package/src/countries.js +175 -175
- package/src/country-context.d.ts +10 -10
- package/src/country-context.js +22 -22
- package/src/currency.d.ts +14 -14
- package/src/currency.js +66 -66
- package/src/geo.js +169 -169
- package/src/geography-central-asia.js +42 -42
- package/src/geography-detection.d.ts +14 -14
- package/src/geography-detection.js +64 -64
- package/src/geography-display.d.ts +13 -13
- package/src/geography-display.js +113 -113
- package/src/geography.js +210 -210
- package/src/hiring-advanced.js +147 -147
- package/src/hiring-candidate-fields.d.ts +9 -9
- package/src/hiring-candidate-fields.js +92 -92
- package/src/hiring-context.d.ts +64 -64
- package/src/hiring-context.js +412 -412
- package/src/hiring-language-extensions.d.ts +19 -19
- package/src/hiring-language-extensions.js +139 -139
- package/src/hiring-languages.js +135 -135
- package/src/hiring-location-fields.d.ts +3 -3
- package/src/hiring-location-fields.js +53 -53
- package/src/hiring-professions-ro.js +191 -191
- package/src/hiring-professions.js +437 -437
- package/src/hiring-requirements.d.ts +16 -16
- package/src/hiring-requirements.js +170 -170
- package/src/hiring-salary-context.d.ts +27 -27
- package/src/hiring-salary-context.js +76 -76
- package/src/hiring-semantics.d.ts +29 -29
- package/src/hiring-semantics.js +218 -218
- package/src/hiring-skills.d.ts +21 -21
- package/src/hiring-skills.js +315 -315
- package/src/hiring-source-aliases.d.ts +32 -32
- package/src/hiring-source-aliases.js +185 -185
- package/src/hiring-source-semantics.d.ts +31 -31
- package/src/hiring-source-semantics.js +293 -293
- package/src/hiring-temporal.d.ts +15 -15
- package/src/hiring-temporal.js +67 -67
- package/src/hiring-vacancy-fields.d.ts +5 -5
- package/src/hiring-vacancy-fields.js +23 -23
- package/src/hiring-work-semantics.d.ts +34 -34
- package/src/hiring-work-semantics.js +72 -72
- package/src/hiring.js +180 -180
- package/src/housing-address.d.ts +18 -18
- package/src/housing-address.js +217 -210
- package/src/housing-commercial.d.ts +1 -0
- package/src/housing-commercial.js +9 -0
- package/src/housing-context.d.ts +36 -36
- package/src/housing-context.js +183 -183
- package/src/housing-features.js +30 -30
- package/src/housing-intent.d.ts +9 -9
- package/src/housing-intent.js +132 -132
- package/src/housing-listing-fields.js +131 -131
- package/src/housing-money.d.ts +7 -7
- package/src/housing-money.js +102 -102
- package/src/housing-source-aliases.d.ts +8 -8
- package/src/housing-source-aliases.js +51 -51
- package/src/housing-structured.d.ts +55 -55
- package/src/housing-structured.js +219 -219
- package/src/housing.js +200 -200
- package/src/index.js +45 -44
- package/src/kz-location-extensions.js +307 -307
- package/src/landmarks.js +21 -21
- package/src/lexicon-core.js +58 -58
- package/src/location-merge.js +106 -106
- package/src/locations.js +456 -456
- package/src/money-core.d.ts +10 -10
- package/src/money-core.js +81 -81
- package/src/money-lexicon.d.ts +5 -5
- package/src/money-lexicon.js +79 -79
- package/src/money.js +154 -154
- package/src/normalization.js +384 -384
- package/src/odesa-metropolitan.js +144 -144
- package/src/romania-geography.js +47 -47
- package/src/tashkent-colloquial.js +80 -72
- package/src/tashkent-housing-geography.d.ts +38 -38
- package/src/tashkent-housing-geography.js +211 -211
- package/src/tashkent-pois.js +215 -215
- package/src/tashkent-residential-complexes.js +272 -272
- package/src/ua-location-extensions-major.js +118 -118
- package/src/ua-location-extensions-metro.js +9 -9
- package/src/ua-location-extensions-regional.js +246 -246
- package/src/ua-secondary-cities.d.ts +2 -2
- package/src/ua-secondary-cities.js +57 -57
- package/src/ukraine.js +75 -75
- package/src/uz-location-extensions.js +259 -259
package/src/contact.d.ts
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
export type PhoneLikeSpan = Readonly<{
|
|
2
|
-
start: number;
|
|
3
|
-
end: number;
|
|
4
|
-
raw: string;
|
|
5
|
-
digits: string;
|
|
6
|
-
}>;
|
|
7
|
-
|
|
8
|
-
export type ParsedPhoneNumber = Readonly<{
|
|
9
|
-
start: number;
|
|
10
|
-
end: number;
|
|
11
|
-
raw: string;
|
|
12
|
-
digits: string;
|
|
13
|
-
number: string;
|
|
14
|
-
nationalNumber: string;
|
|
15
|
-
national: string;
|
|
16
|
-
international: string;
|
|
17
|
-
country: string | null;
|
|
18
|
-
countryCallingCode: string;
|
|
19
|
-
extension: string | null;
|
|
20
|
-
valid: boolean;
|
|
21
|
-
possible: boolean;
|
|
22
|
-
}>;
|
|
23
|
-
|
|
24
|
-
export type ParsePhoneOptions = Readonly<{
|
|
25
|
-
countryHint?: string | null;
|
|
26
|
-
includePossible?: boolean;
|
|
27
|
-
}>;
|
|
28
|
-
|
|
29
|
-
export type TelegramContact = Readonly<{
|
|
30
|
-
start: number;
|
|
31
|
-
end: number;
|
|
32
|
-
raw: string;
|
|
33
|
-
username: string;
|
|
34
|
-
handle: string;
|
|
35
|
-
url: string;
|
|
36
|
-
source: 'mention' | 'url' | 'tg';
|
|
37
|
-
}>;
|
|
38
|
-
|
|
39
|
-
export function findPhoneLikeSpans(value: unknown): readonly PhoneLikeSpan[];
|
|
40
|
-
export function maskPhoneLikeSpans(value: unknown, replacement?: string): string;
|
|
41
|
-
export function parsePhoneNumbers(value: unknown, options?: ParsePhoneOptions): readonly ParsedPhoneNumber[];
|
|
42
|
-
export function normalizePhone(value: unknown, options?: ParsePhoneOptions): ParsedPhoneNumber | null;
|
|
43
|
-
export function findTelegramContacts(value: unknown): readonly TelegramContact[];
|
|
44
|
-
export function normalizeTelegramContact(value: unknown): TelegramContact | null;
|
|
1
|
+
export type PhoneLikeSpan = Readonly<{
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
raw: string;
|
|
5
|
+
digits: string;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
export type ParsedPhoneNumber = Readonly<{
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
raw: string;
|
|
12
|
+
digits: string;
|
|
13
|
+
number: string;
|
|
14
|
+
nationalNumber: string;
|
|
15
|
+
national: string;
|
|
16
|
+
international: string;
|
|
17
|
+
country: string | null;
|
|
18
|
+
countryCallingCode: string;
|
|
19
|
+
extension: string | null;
|
|
20
|
+
valid: boolean;
|
|
21
|
+
possible: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export type ParsePhoneOptions = Readonly<{
|
|
25
|
+
countryHint?: string | null;
|
|
26
|
+
includePossible?: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type TelegramContact = Readonly<{
|
|
30
|
+
start: number;
|
|
31
|
+
end: number;
|
|
32
|
+
raw: string;
|
|
33
|
+
username: string;
|
|
34
|
+
handle: string;
|
|
35
|
+
url: string;
|
|
36
|
+
source: 'mention' | 'url' | 'tg';
|
|
37
|
+
}>;
|
|
38
|
+
|
|
39
|
+
export function findPhoneLikeSpans(value: unknown): readonly PhoneLikeSpan[];
|
|
40
|
+
export function maskPhoneLikeSpans(value: unknown, replacement?: string): string;
|
|
41
|
+
export function parsePhoneNumbers(value: unknown, options?: ParsePhoneOptions): readonly ParsedPhoneNumber[];
|
|
42
|
+
export function normalizePhone(value: unknown, options?: ParsePhoneOptions): ParsedPhoneNumber | null;
|
|
43
|
+
export function findTelegramContacts(value: unknown): readonly TelegramContact[];
|
|
44
|
+
export function normalizeTelegramContact(value: unknown): TelegramContact | null;
|
package/src/contact.js
CHANGED
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
import { parsePhoneNumberFromString } from 'libphonenumber-js/min';
|
|
2
|
-
|
|
3
|
-
// Broad phone-like detection used by other parsers for exclusion/classification.
|
|
4
|
-
// It deliberately stays tolerant and does not validate against a country plan.
|
|
5
|
-
const PHONE_LIKE_RE = /\+?\d(?:[\t \u00a0().-]*\d){9,}/g;
|
|
6
|
-
|
|
7
|
-
// Contact extraction may start from shorter national formats, but candidates are
|
|
8
|
-
// only returned after libphonenumber validation.
|
|
9
|
-
const PHONE_CANDIDATE_RE = /\+?\d(?:[\t \u00a0().-]*\d){6,}(?:[\t \u00a0]*(?:ext\.?|extension|x|доб\.?|дод\.?)\s*\d{1,6})?/giu;
|
|
10
|
-
const PHONE_EXTENSION_RE = /[\t \u00a0]*(?:ext\.?|extension|x|доб\.?|дод\.?)\s*(\d{1,6})$/iu;
|
|
11
|
-
|
|
12
|
-
const TELEGRAM_USERNAME_RE = /^[A-Za-z0-9_]{5,32}$/;
|
|
13
|
-
const TELEGRAM_LINK_RE = /(?:https?:\/\/)?(?:t\.me|telegram\.me|telegram\.dog)\/([A-Za-z0-9_]{5,32})(?:\/[0-9]+)?(?:[/?#][^\s]*)?/giu;
|
|
14
|
-
const TELEGRAM_TG_RE = /tg:\/\/resolve\?[^\s]*?\bdomain=([A-Za-z0-9_]{5,32})\b[^\s]*/giu;
|
|
15
|
-
const TELEGRAM_MENTION_RE = /(^|[^\p{L}\p{N}_@])@([A-Za-z0-9_]{5,32})\b/gu;
|
|
16
|
-
|
|
17
|
-
function normalizedCountryHint(value) {
|
|
18
|
-
const country = String(value || '').trim().toUpperCase();
|
|
19
|
-
return /^[A-Z]{2}$/.test(country) ? country : undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function splitPhoneExtension(raw) {
|
|
23
|
-
const match = String(raw || '').match(PHONE_EXTENSION_RE);
|
|
24
|
-
if (!match) return { base: String(raw || '').trim(), extension: null };
|
|
25
|
-
return {
|
|
26
|
-
base: String(raw || '').slice(0, match.index).trim(),
|
|
27
|
-
extension: match[1] || null,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function findPhoneLikeSpans(value) {
|
|
32
|
-
const text = String(value || '');
|
|
33
|
-
const spans = [];
|
|
34
|
-
for (const match of text.matchAll(PHONE_LIKE_RE)) {
|
|
35
|
-
const raw = match[0];
|
|
36
|
-
const digits = raw.replace(/\D/g, '');
|
|
37
|
-
if (digits.length < 10) continue;
|
|
38
|
-
const start = match.index ?? 0;
|
|
39
|
-
spans.push(Object.freeze({
|
|
40
|
-
start,
|
|
41
|
-
end: start + raw.length,
|
|
42
|
-
raw,
|
|
43
|
-
digits,
|
|
44
|
-
}));
|
|
45
|
-
}
|
|
46
|
-
return Object.freeze(spans);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function maskPhoneLikeSpans(value, replacement = ' ') {
|
|
50
|
-
const text = String(value || '');
|
|
51
|
-
const spans = findPhoneLikeSpans(text);
|
|
52
|
-
if (!spans.length) return text;
|
|
53
|
-
|
|
54
|
-
let out = '';
|
|
55
|
-
let cursor = 0;
|
|
56
|
-
for (const span of spans) {
|
|
57
|
-
out += text.slice(cursor, span.start);
|
|
58
|
-
out += replacement;
|
|
59
|
-
cursor = span.end;
|
|
60
|
-
}
|
|
61
|
-
return out + text.slice(cursor);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function parsePhoneNumbers(value, options = {}) {
|
|
65
|
-
const text = String(value || '');
|
|
66
|
-
const countryHint = normalizedCountryHint(options.countryHint);
|
|
67
|
-
const includePossible = options.includePossible === true;
|
|
68
|
-
const out = [];
|
|
69
|
-
const seen = new Set();
|
|
70
|
-
|
|
71
|
-
for (const match of text.matchAll(PHONE_CANDIDATE_RE)) {
|
|
72
|
-
const raw = match[0].trim();
|
|
73
|
-
const { base, extension } = splitPhoneExtension(raw);
|
|
74
|
-
const parsed = parsePhoneNumberFromString(base, countryHint);
|
|
75
|
-
if (!parsed) continue;
|
|
76
|
-
const valid = parsed.isValid();
|
|
77
|
-
const possible = parsed.isPossible();
|
|
78
|
-
if (!valid && !(includePossible && possible)) continue;
|
|
79
|
-
|
|
80
|
-
const key = `${parsed.number}:${extension || parsed.ext || ''}`;
|
|
81
|
-
if (seen.has(key)) continue;
|
|
82
|
-
seen.add(key);
|
|
83
|
-
|
|
84
|
-
const start = match.index ?? 0;
|
|
85
|
-
out.push(Object.freeze({
|
|
86
|
-
start,
|
|
87
|
-
end: start + match[0].length,
|
|
88
|
-
raw,
|
|
89
|
-
digits: base.replace(/\D/g, ''),
|
|
90
|
-
number: parsed.number,
|
|
91
|
-
nationalNumber: parsed.nationalNumber,
|
|
92
|
-
national: parsed.formatNational(),
|
|
93
|
-
international: parsed.formatInternational(),
|
|
94
|
-
country: parsed.country || null,
|
|
95
|
-
countryCallingCode: parsed.countryCallingCode,
|
|
96
|
-
extension: extension || parsed.ext || null,
|
|
97
|
-
valid,
|
|
98
|
-
possible,
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return Object.freeze(out);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function normalizePhone(value, options = {}) {
|
|
106
|
-
return parsePhoneNumbers(value, options)[0] || null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function telegramContact(username, raw, start, source) {
|
|
110
|
-
const normalized = String(username || '').replace(/^@/, '');
|
|
111
|
-
if (!TELEGRAM_USERNAME_RE.test(normalized)) return null;
|
|
112
|
-
return Object.freeze({
|
|
113
|
-
start,
|
|
114
|
-
end: start + raw.length,
|
|
115
|
-
raw,
|
|
116
|
-
username: normalized,
|
|
117
|
-
handle: `@${normalized}`,
|
|
118
|
-
url: `https://t.me/${normalized}`,
|
|
119
|
-
source,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Extract public Telegram username contacts from mentions and links.
|
|
125
|
-
* Message/channel transport parsing stays in consumers; this helper owns only
|
|
126
|
-
* reusable contact normalization.
|
|
127
|
-
*/
|
|
128
|
-
export function findTelegramContacts(value) {
|
|
129
|
-
const text = String(value || '');
|
|
130
|
-
const found = [];
|
|
131
|
-
const seen = new Set();
|
|
132
|
-
|
|
133
|
-
const push = (username, raw, start, source) => {
|
|
134
|
-
const contact = telegramContact(username, raw, start, source);
|
|
135
|
-
if (!contact) return;
|
|
136
|
-
const key = contact.username.toLowerCase();
|
|
137
|
-
if (seen.has(key)) return;
|
|
138
|
-
seen.add(key);
|
|
139
|
-
found.push(contact);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
for (const match of text.matchAll(TELEGRAM_LINK_RE)) {
|
|
143
|
-
push(match[1], match[0], match.index ?? 0, 'url');
|
|
144
|
-
}
|
|
145
|
-
for (const match of text.matchAll(TELEGRAM_TG_RE)) {
|
|
146
|
-
push(match[1], match[0], match.index ?? 0, 'tg');
|
|
147
|
-
}
|
|
148
|
-
for (const match of text.matchAll(TELEGRAM_MENTION_RE)) {
|
|
149
|
-
const prefix = match[1] || '';
|
|
150
|
-
const raw = `@${match[2]}`;
|
|
151
|
-
push(match[2], raw, (match.index ?? 0) + prefix.length, 'mention');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return Object.freeze(found.sort((a, b) => a.start - b.start));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function normalizeTelegramContact(value) {
|
|
158
|
-
return findTelegramContacts(value)[0] || null;
|
|
159
|
-
}
|
|
1
|
+
import { parsePhoneNumberFromString } from 'libphonenumber-js/min';
|
|
2
|
+
|
|
3
|
+
// Broad phone-like detection used by other parsers for exclusion/classification.
|
|
4
|
+
// It deliberately stays tolerant and does not validate against a country plan.
|
|
5
|
+
const PHONE_LIKE_RE = /\+?\d(?:[\t \u00a0().-]*\d){9,}/g;
|
|
6
|
+
|
|
7
|
+
// Contact extraction may start from shorter national formats, but candidates are
|
|
8
|
+
// only returned after libphonenumber validation.
|
|
9
|
+
const PHONE_CANDIDATE_RE = /\+?\d(?:[\t \u00a0().-]*\d){6,}(?:[\t \u00a0]*(?:ext\.?|extension|x|доб\.?|дод\.?)\s*\d{1,6})?/giu;
|
|
10
|
+
const PHONE_EXTENSION_RE = /[\t \u00a0]*(?:ext\.?|extension|x|доб\.?|дод\.?)\s*(\d{1,6})$/iu;
|
|
11
|
+
|
|
12
|
+
const TELEGRAM_USERNAME_RE = /^[A-Za-z0-9_]{5,32}$/;
|
|
13
|
+
const TELEGRAM_LINK_RE = /(?:https?:\/\/)?(?:t\.me|telegram\.me|telegram\.dog)\/([A-Za-z0-9_]{5,32})(?:\/[0-9]+)?(?:[/?#][^\s]*)?/giu;
|
|
14
|
+
const TELEGRAM_TG_RE = /tg:\/\/resolve\?[^\s]*?\bdomain=([A-Za-z0-9_]{5,32})\b[^\s]*/giu;
|
|
15
|
+
const TELEGRAM_MENTION_RE = /(^|[^\p{L}\p{N}_@])@([A-Za-z0-9_]{5,32})\b/gu;
|
|
16
|
+
|
|
17
|
+
function normalizedCountryHint(value) {
|
|
18
|
+
const country = String(value || '').trim().toUpperCase();
|
|
19
|
+
return /^[A-Z]{2}$/.test(country) ? country : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function splitPhoneExtension(raw) {
|
|
23
|
+
const match = String(raw || '').match(PHONE_EXTENSION_RE);
|
|
24
|
+
if (!match) return { base: String(raw || '').trim(), extension: null };
|
|
25
|
+
return {
|
|
26
|
+
base: String(raw || '').slice(0, match.index).trim(),
|
|
27
|
+
extension: match[1] || null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function findPhoneLikeSpans(value) {
|
|
32
|
+
const text = String(value || '');
|
|
33
|
+
const spans = [];
|
|
34
|
+
for (const match of text.matchAll(PHONE_LIKE_RE)) {
|
|
35
|
+
const raw = match[0];
|
|
36
|
+
const digits = raw.replace(/\D/g, '');
|
|
37
|
+
if (digits.length < 10) continue;
|
|
38
|
+
const start = match.index ?? 0;
|
|
39
|
+
spans.push(Object.freeze({
|
|
40
|
+
start,
|
|
41
|
+
end: start + raw.length,
|
|
42
|
+
raw,
|
|
43
|
+
digits,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
return Object.freeze(spans);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function maskPhoneLikeSpans(value, replacement = ' ') {
|
|
50
|
+
const text = String(value || '');
|
|
51
|
+
const spans = findPhoneLikeSpans(text);
|
|
52
|
+
if (!spans.length) return text;
|
|
53
|
+
|
|
54
|
+
let out = '';
|
|
55
|
+
let cursor = 0;
|
|
56
|
+
for (const span of spans) {
|
|
57
|
+
out += text.slice(cursor, span.start);
|
|
58
|
+
out += replacement;
|
|
59
|
+
cursor = span.end;
|
|
60
|
+
}
|
|
61
|
+
return out + text.slice(cursor);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parsePhoneNumbers(value, options = {}) {
|
|
65
|
+
const text = String(value || '');
|
|
66
|
+
const countryHint = normalizedCountryHint(options.countryHint);
|
|
67
|
+
const includePossible = options.includePossible === true;
|
|
68
|
+
const out = [];
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
|
|
71
|
+
for (const match of text.matchAll(PHONE_CANDIDATE_RE)) {
|
|
72
|
+
const raw = match[0].trim();
|
|
73
|
+
const { base, extension } = splitPhoneExtension(raw);
|
|
74
|
+
const parsed = parsePhoneNumberFromString(base, countryHint);
|
|
75
|
+
if (!parsed) continue;
|
|
76
|
+
const valid = parsed.isValid();
|
|
77
|
+
const possible = parsed.isPossible();
|
|
78
|
+
if (!valid && !(includePossible && possible)) continue;
|
|
79
|
+
|
|
80
|
+
const key = `${parsed.number}:${extension || parsed.ext || ''}`;
|
|
81
|
+
if (seen.has(key)) continue;
|
|
82
|
+
seen.add(key);
|
|
83
|
+
|
|
84
|
+
const start = match.index ?? 0;
|
|
85
|
+
out.push(Object.freeze({
|
|
86
|
+
start,
|
|
87
|
+
end: start + match[0].length,
|
|
88
|
+
raw,
|
|
89
|
+
digits: base.replace(/\D/g, ''),
|
|
90
|
+
number: parsed.number,
|
|
91
|
+
nationalNumber: parsed.nationalNumber,
|
|
92
|
+
national: parsed.formatNational(),
|
|
93
|
+
international: parsed.formatInternational(),
|
|
94
|
+
country: parsed.country || null,
|
|
95
|
+
countryCallingCode: parsed.countryCallingCode,
|
|
96
|
+
extension: extension || parsed.ext || null,
|
|
97
|
+
valid,
|
|
98
|
+
possible,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return Object.freeze(out);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function normalizePhone(value, options = {}) {
|
|
106
|
+
return parsePhoneNumbers(value, options)[0] || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function telegramContact(username, raw, start, source) {
|
|
110
|
+
const normalized = String(username || '').replace(/^@/, '');
|
|
111
|
+
if (!TELEGRAM_USERNAME_RE.test(normalized)) return null;
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
start,
|
|
114
|
+
end: start + raw.length,
|
|
115
|
+
raw,
|
|
116
|
+
username: normalized,
|
|
117
|
+
handle: `@${normalized}`,
|
|
118
|
+
url: `https://t.me/${normalized}`,
|
|
119
|
+
source,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract public Telegram username contacts from mentions and links.
|
|
125
|
+
* Message/channel transport parsing stays in consumers; this helper owns only
|
|
126
|
+
* reusable contact normalization.
|
|
127
|
+
*/
|
|
128
|
+
export function findTelegramContacts(value) {
|
|
129
|
+
const text = String(value || '');
|
|
130
|
+
const found = [];
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
|
|
133
|
+
const push = (username, raw, start, source) => {
|
|
134
|
+
const contact = telegramContact(username, raw, start, source);
|
|
135
|
+
if (!contact) return;
|
|
136
|
+
const key = contact.username.toLowerCase();
|
|
137
|
+
if (seen.has(key)) return;
|
|
138
|
+
seen.add(key);
|
|
139
|
+
found.push(contact);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
for (const match of text.matchAll(TELEGRAM_LINK_RE)) {
|
|
143
|
+
push(match[1], match[0], match.index ?? 0, 'url');
|
|
144
|
+
}
|
|
145
|
+
for (const match of text.matchAll(TELEGRAM_TG_RE)) {
|
|
146
|
+
push(match[1], match[0], match.index ?? 0, 'tg');
|
|
147
|
+
}
|
|
148
|
+
for (const match of text.matchAll(TELEGRAM_MENTION_RE)) {
|
|
149
|
+
const prefix = match[1] || '';
|
|
150
|
+
const raw = `@${match[2]}`;
|
|
151
|
+
push(match[2], raw, (match.index ?? 0) + prefix.length, 'mention');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return Object.freeze(found.sort((a, b) => a.start - b.start));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function normalizeTelegramContact(value) {
|
|
158
|
+
return findTelegramContacts(value)[0] || null;
|
|
159
|
+
}
|