Skip to content

Commit d229141

Browse files
committed
feat: add LatLng and international phone custom matchers
Remove experimental latlng prop and instead use custom matchers to provide LatLngMatcher. Also add IntlPhoneMatcher and country-specific phone matchers also using custom matchers functionality. BREAKING CHANGE: Prop latlng removed - supply LatLngMatcher to matchers prop instead closes joshswan#25
1 parent 6012dad commit d229141

File tree

9 files changed

+165
-124
lines changed

9 files changed

+165
-124
lines changed

src/__tests__/__snapshots__/index.test.tsx.snap

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,6 @@ exports[`<Autolink /> does not wrap a hashtag in a link Text node when hashtag p
8080
</Text>
8181
`;
8282

83-
exports[`<Autolink /> does not wrap a latitude/longitude pair in a link Text node when latlng prop disabled 1`] = `
84-
<Text>
85-
<Text>
86-
34.0522, -118.2437
87-
</Text>
88-
</Text>
89-
`;
90-
9183
exports[`<Autolink /> does not wrap a mention/handle in a link Text node when mention prop disabled 1`] = `
9284
<Text>
9385
<Text>
@@ -198,6 +190,22 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
198190
</Text>
199191
`;
200192

193+
exports[`<Autolink /> matchers wraps text based on supplied custom matchers 1`] = `
194+
<Text>
195+
<Text
196+
onLongPress={[Function]}
197+
onPress={[Function]}
198+
style={
199+
Object {
200+
"color": "#0E7AFE",
201+
}
202+
}
203+
>
204+
34.0522, -118.2437
205+
</Text>
206+
</Text>
207+
`;
208+
201209
exports[`<Autolink /> matches top-level domain url when wwwMatches enabled 1`] = `
202210
<Text>
203211
<Text
@@ -362,22 +370,6 @@ exports[`<Autolink /> wraps a hashtag in a link Text node when hashtag prop enab
362370
</Text>
363371
`;
364372

365-
exports[`<Autolink /> wraps a latitude/longitude pair in a link Text node when latlng prop enabled 1`] = `
366-
<Text>
367-
<Text
368-
onLongPress={[Function]}
369-
onPress={[Function]}
370-
style={
371-
Object {
372-
"color": "#0E7AFE",
373-
}
374-
}
375-
>
376-
34.0522, -118.2437
377-
</Text>
378-
</Text>
379-
`;
380-
381373
exports[`<Autolink /> wraps a mention/handle in a link Text node when mention prop enabled 1`] = `
382374
<Text>
383375
<Text

src/__tests__/index.test.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React from 'react';
1010
import { Text, View } from 'react-native';
1111
import renderer from 'react-test-renderer';
1212
import Autolink from '..';
13+
import { LatLngMatcher } from '../matchers';
1314

1415
describe('<Autolink />', () => {
1516
test('renders a Text node', () => {
@@ -241,15 +242,14 @@ describe('<Autolink />', () => {
241242
});
242243

243244
/**
244-
* EXPERIMENTAL
245+
* Custom matchers
245246
*/
246-
test('wraps a latitude/longitude pair in a link Text node when latlng prop enabled', () => {
247-
const tree = renderer.create(<Autolink text="34.0522, -118.2437" latlng />).toJSON();
248-
expect(tree).toMatchSnapshot();
249-
});
250-
251-
test('does not wrap a latitude/longitude pair in a link Text node when latlng prop disabled', () => {
252-
const tree = renderer.create(<Autolink text="34.0522, -118.2437" latlng={false} />).toJSON();
253-
expect(tree).toMatchSnapshot();
247+
describe('matchers', () => {
248+
test('wraps text based on supplied custom matchers', () => {
249+
const tree = renderer
250+
.create(<Autolink text="34.0522, -118.2437" matchers={[LatLngMatcher]} />)
251+
.toJSON();
252+
expect(tree).toMatchSnapshot();
253+
});
254254
});
255255
});

src/index.tsx

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ import {
2727
TextProps,
2828
} from 'react-native';
2929
import * as Truncate from './truncate';
30-
import { Matchers, MatcherId, LatLngMatch } from './matchers';
3130
import { CustomMatch, CustomMatcher } from './CustomMatch';
3231
import { PropsOf } from './types';
3332

3433
export * from './CustomMatch';
34+
export * from './matchers';
3535

3636
const tagBuilder = new AnchorTagBuilder();
3737

@@ -45,7 +45,6 @@ interface AutolinkProps<C extends React.ComponentType = React.ComponentType> {
4545
component?: C;
4646
email?: boolean;
4747
hashtag?: false | 'facebook' | 'instagram' | 'twitter';
48-
latlng?: boolean;
4948
linkProps?: TextProps;
5049
linkStyle?: StyleProp<TextStyle>;
5150
matchers?: CustomMatcher[];
@@ -102,7 +101,6 @@ export default class Autolink<C extends React.ComponentType = typeof Text> exten
102101
static defaultProps = {
103102
email: true,
104103
hashtag: false,
105-
latlng: false,
106104
linkProps: {},
107105
mention: false,
108106
phone: true,
@@ -187,16 +185,6 @@ export default class Autolink<C extends React.ComponentType = typeof Text> exten
187185
return [match.getMatchedText()];
188186
}
189187
}
190-
case 'latlng': {
191-
const latlng = (match as LatLngMatch).getLatLng();
192-
const query = latlng.replace(/\s/g, '');
193-
194-
return [
195-
Platform.OS === 'ios'
196-
? `http://maps.apple.com/?q=${encodeURIComponent(latlng)}&ll=${query}`
197-
: `https://www.google.com/maps/search/?api=1&query=${query}`,
198-
];
199-
}
200188
case 'mention': {
201189
const username = (match as MentionMatch).getMention();
202190

@@ -225,12 +213,8 @@ export default class Autolink<C extends React.ComponentType = typeof Text> exten
225213
return [`tel:${number}`];
226214
}
227215
}
228-
case 'userCustom':
229-
case 'url': {
230-
return [match.getAnchorHref()];
231-
}
232216
default: {
233-
return [match.getMatchedText()];
217+
return [match.getAnchorHref() ?? match.getAnchorText()];
234218
}
235219
}
236220
}
@@ -264,7 +248,6 @@ export default class Autolink<C extends React.ComponentType = typeof Text> exten
264248
component = Text,
265249
email,
266250
hashtag,
267-
latlng,
268251
linkProps,
269252
linkStyle,
270253
matchers = [],
@@ -318,26 +301,6 @@ export default class Autolink<C extends React.ComponentType = typeof Text> exten
318301
},
319302
});
320303

321-
// Custom matchers
322-
Matchers.forEach((matcher) => {
323-
// eslint-disable-next-line react/destructuring-assignment
324-
if (this.props[matcher.id as MatcherId]) {
325-
linkedText = linkedText.replace(matcher.regex, (...args) => {
326-
const token = generateToken();
327-
const matchedText = args[0];
328-
329-
matches[token] = new matcher.Match({
330-
tagBuilder,
331-
matchedText,
332-
offset: args[args.length - 2],
333-
[matcher.id as MatcherId]: matchedText,
334-
});
335-
336-
return token;
337-
});
338-
}
339-
});
340-
341304
// User-specified custom matchers
342305
matchers.forEach((matcher) => {
343306
linkedText = linkedText.replace(matcher.pattern, (...replacerArgs) => {

src/matchers.ts

Lines changed: 0 additions & 52 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*!
2+
* React Native Autolink
3+
*
4+
* Copyright 2016-2021 Josh Swan
5+
* Released under the MIT license
6+
* https://github.com/joshswan/react-native-autolink/blob/master/LICENSE
7+
*/
8+
9+
import { LatLngMatcher } from '..';
10+
11+
describe('Location matchers', () => {
12+
describe('LatLngMatcher', () => {
13+
test('matches latitude/longitude pair in text', () => {
14+
const text = 'Location is 34.0522, -118.2437.';
15+
expect(text.replace(LatLngMatcher.pattern, 'MATCH')).toBe('Location is MATCH.');
16+
});
17+
});
18+
});

src/matchers/__tests__/phone.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*!
2+
* React Native Autolink
3+
*
4+
* Copyright 2016-2021 Josh Swan
5+
* Released under the MIT license
6+
* https://github.com/joshswan/react-native-autolink/blob/master/LICENSE
7+
*/
8+
9+
import { IntlPhoneMatcher, PhoneMatchersByCountry } from '..';
10+
11+
const getText = (number: string) => `Phone number is ${number}.`;
12+
const resultText = 'Phone number is MATCH.';
13+
14+
describe('Phone matchers', () => {
15+
describe('Generic', () => {
16+
test('matches common international phone numbers', () => {
17+
expect(getText('+12317527630').replace(IntlPhoneMatcher.pattern, 'MATCH')).toBe(resultText);
18+
expect(getText('+4915789173959').replace(IntlPhoneMatcher.pattern, 'MATCH')).toBe(resultText);
19+
expect(getText('+33699520828').replace(IntlPhoneMatcher.pattern, 'MATCH')).toBe(resultText);
20+
expect(getText('+48571775914').replace(IntlPhoneMatcher.pattern, 'MATCH')).toBe(resultText);
21+
expect(getText('+447487428082').replace(IntlPhoneMatcher.pattern, 'MATCH')).toBe(resultText);
22+
});
23+
});
24+
25+
describe('PhoneMatchersByCountry', () => {
26+
test('matches French phone numbers', () => {
27+
expect(getText('+33699520828').replace(PhoneMatchersByCountry.FR.pattern, 'MATCH')).toBe(
28+
resultText,
29+
);
30+
});
31+
32+
test('matches Polish phone numbers', () => {
33+
expect(getText('+48571775914').replace(PhoneMatchersByCountry.PL.pattern, 'MATCH')).toBe(
34+
resultText,
35+
);
36+
});
37+
38+
test('matches UK phone numbers', () => {
39+
expect(getText('+447487428082').replace(PhoneMatchersByCountry.UK.pattern, 'MATCH')).toBe(
40+
resultText,
41+
);
42+
});
43+
44+
test('matches US phone numbers', () => {
45+
expect(getText('+12317527630').replace(PhoneMatchersByCountry.US.pattern, 'MATCH')).toBe(
46+
resultText,
47+
);
48+
});
49+
});
50+
});

src/matchers/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*!
2+
* React Native Autolink
3+
*
4+
* Copyright 2016-2021 Josh Swan
5+
* Released under the MIT license
6+
* https://github.com/joshswan/react-native-autolink/blob/master/LICENSE
7+
*/
8+
9+
export * from './location';
10+
export * from './phone';

src/matchers/location.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*!
2+
* React Native Autolink
3+
*
4+
* Copyright 2016-2021 Josh Swan
5+
* Released under the MIT license
6+
* https://github.com/joshswan/react-native-autolink/blob/master/LICENSE
7+
*/
8+
9+
import { Platform } from 'react-native';
10+
import type { CustomMatcher } from '../CustomMatch';
11+
12+
export const LatLngMatcher: CustomMatcher = {
13+
pattern: /[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)/g,
14+
type: 'latlng',
15+
getLinkUrl: ([latlng]) => {
16+
const query = latlng.replace(/\s/g, '');
17+
return Platform.OS === 'ios' || Platform.OS === 'macos'
18+
? `http://maps.apple.com/?q=${encodeURIComponent(latlng)}&ll=${query}`
19+
: `https://www.google.com/maps/search/?api=1&query=${query}`;
20+
},
21+
};

src/matchers/phone.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*!
2+
* React Native Autolink
3+
*
4+
* Copyright 2016-2021 Josh Swan
5+
* Released under the MIT license
6+
* https://github.com/joshswan/react-native-autolink/blob/master/LICENSE
7+
*/
8+
9+
import type { CustomMatcher } from '../CustomMatch';
10+
11+
export const IntlPhoneMatcher: CustomMatcher = {
12+
pattern: /\+(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\d{1,14}/g,
13+
type: 'phone-intl',
14+
getLinkUrl: ([number]) => `tel:${number}`,
15+
};
16+
17+
// NOTE: These patterns don't support extensions (i.e. "x" or "ext")
18+
const patternsByCountry = {
19+
// France
20+
FR: /(\+33|0|0033)[1-9]\d{8}/g,
21+
// Poland
22+
PL: /(?:(?:(?:\+|00)?48)|(?:\(\+?48\)))?(?:1[2-8]|2[2-69]|3[2-49]|4[1-8]|5[0-9]|6[0-35-9]|[7-8][1-9]|9[145])\d{7}/g,
23+
// United Kingdom
24+
UK: /(?:(?:\(?(?:0(?:0|11)\)?[\s-]?\(?|\+)44\)?[\s-]?(?:\(?0\)?[\s-]?)?)|(?:\(?0))(?:(?:\d{5}\)?[\s-]?\d{4,5})|(?:\d{4}\)?[\s-]?(?:\d{5}|\d{3}[\s-]?\d{3}))|(?:\d{3}\)?[\s-]?\d{3}[\s-]?\d{3,4})|(?:\d{2}\)?[\s-]?\d{4}[\s-]?\d{4}))/g,
25+
// United States
26+
US: /(?:(?:\+?1\s*(?:[.-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[.-]\s*)?)?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\s*(?:[.-]\s*)?([0-9]{4})/g,
27+
};
28+
29+
export const PhoneMatchersByCountry = Object.entries(patternsByCountry).reduce(
30+
(matchers, [countryCode, pattern]) => ({
31+
...matchers,
32+
[countryCode]: {
33+
pattern,
34+
type: `phone-${countryCode}`,
35+
getLinkUrl: ([number]) => `tel:${number.replace(/[^\d+]/g, '')}`,
36+
} as CustomMatcher,
37+
}),
38+
{} as Record<keyof typeof patternsByCountry, CustomMatcher>,
39+
);

0 commit comments

Comments
 (0)