Skip to content

Commit

Permalink
feat: add LatLng and international phone custom matchers
Browse files Browse the repository at this point in the history
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 #25
  • Loading branch information
joshswan committed Mar 28, 2021
1 parent 6012dad commit d229141
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 124 deletions.
40 changes: 16 additions & 24 deletions src/__tests__/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,6 @@ exports[`<Autolink /> does not wrap a hashtag in a link Text node when hashtag p
</Text>
`;

exports[`<Autolink /> does not wrap a latitude/longitude pair in a link Text node when latlng prop disabled 1`] = `
<Text>
<Text>
34.0522, -118.2437
</Text>
</Text>
`;

exports[`<Autolink /> does not wrap a mention/handle in a link Text node when mention prop disabled 1`] = `
<Text>
<Text>
Expand Down Expand Up @@ -198,6 +190,22 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
</Text>
`;

exports[`<Autolink /> matchers wraps text based on supplied custom matchers 1`] = `
<Text>
<Text
onLongPress={[Function]}
onPress={[Function]}
style={
Object {
"color": "#0E7AFE",
}
}
>
34.0522, -118.2437
</Text>
</Text>
`;

exports[`<Autolink /> matches top-level domain url when wwwMatches enabled 1`] = `
<Text>
<Text
Expand Down Expand Up @@ -362,22 +370,6 @@ exports[`<Autolink /> wraps a hashtag in a link Text node when hashtag prop enab
</Text>
`;

exports[`<Autolink /> wraps a latitude/longitude pair in a link Text node when latlng prop enabled 1`] = `
<Text>
<Text
onLongPress={[Function]}
onPress={[Function]}
style={
Object {
"color": "#0E7AFE",
}
}
>
34.0522, -118.2437
</Text>
</Text>
`;

exports[`<Autolink /> wraps a mention/handle in a link Text node when mention prop enabled 1`] = `
<Text>
<Text
Expand Down
18 changes: 9 additions & 9 deletions src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React from 'react';
import { Text, View } from 'react-native';
import renderer from 'react-test-renderer';
import Autolink from '..';
import { LatLngMatcher } from '../matchers';

describe('<Autolink />', () => {
test('renders a Text node', () => {
Expand Down Expand Up @@ -241,15 +242,14 @@ describe('<Autolink />', () => {
});

/**
* EXPERIMENTAL
* Custom matchers
*/
test('wraps a latitude/longitude pair in a link Text node when latlng prop enabled', () => {
const tree = renderer.create(<Autolink text="34.0522, -118.2437" latlng />).toJSON();
expect(tree).toMatchSnapshot();
});

test('does not wrap a latitude/longitude pair in a link Text node when latlng prop disabled', () => {
const tree = renderer.create(<Autolink text="34.0522, -118.2437" latlng={false} />).toJSON();
expect(tree).toMatchSnapshot();
describe('matchers', () => {
test('wraps text based on supplied custom matchers', () => {
const tree = renderer
.create(<Autolink text="34.0522, -118.2437" matchers={[LatLngMatcher]} />)
.toJSON();
expect(tree).toMatchSnapshot();
});
});
});
41 changes: 2 additions & 39 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import {
TextProps,
} from 'react-native';
import * as Truncate from './truncate';
import { Matchers, MatcherId, LatLngMatch } from './matchers';
import { CustomMatch, CustomMatcher } from './CustomMatch';
import { PropsOf } from './types';

export * from './CustomMatch';
export * from './matchers';

const tagBuilder = new AnchorTagBuilder();

Expand All @@ -45,7 +45,6 @@ interface AutolinkProps<C extends React.ComponentType = React.ComponentType> {
component?: C;
email?: boolean;
hashtag?: false | 'facebook' | 'instagram' | 'twitter';
latlng?: boolean;
linkProps?: TextProps;
linkStyle?: StyleProp<TextStyle>;
matchers?: CustomMatcher[];
Expand Down Expand Up @@ -102,7 +101,6 @@ export default class Autolink<C extends React.ComponentType = typeof Text> exten
static defaultProps = {
email: true,
hashtag: false,
latlng: false,
linkProps: {},
mention: false,
phone: true,
Expand Down Expand Up @@ -187,16 +185,6 @@ export default class Autolink<C extends React.ComponentType = typeof Text> exten
return [match.getMatchedText()];
}
}
case 'latlng': {
const latlng = (match as LatLngMatch).getLatLng();
const query = latlng.replace(/\s/g, '');

return [
Platform.OS === 'ios'
? `http://maps.apple.com/?q=${encodeURIComponent(latlng)}&ll=${query}`
: `https://www.google.com/maps/search/?api=1&query=${query}`,
];
}
case 'mention': {
const username = (match as MentionMatch).getMention();

Expand Down Expand Up @@ -225,12 +213,8 @@ export default class Autolink<C extends React.ComponentType = typeof Text> exten
return [`tel:${number}`];
}
}
case 'userCustom':
case 'url': {
return [match.getAnchorHref()];
}
default: {
return [match.getMatchedText()];
return [match.getAnchorHref() ?? match.getAnchorText()];
}
}
}
Expand Down Expand Up @@ -264,7 +248,6 @@ export default class Autolink<C extends React.ComponentType = typeof Text> exten
component = Text,
email,
hashtag,
latlng,
linkProps,
linkStyle,
matchers = [],
Expand Down Expand Up @@ -318,26 +301,6 @@ export default class Autolink<C extends React.ComponentType = typeof Text> exten
},
});

// Custom matchers
Matchers.forEach((matcher) => {
// eslint-disable-next-line react/destructuring-assignment
if (this.props[matcher.id as MatcherId]) {
linkedText = linkedText.replace(matcher.regex, (...args) => {
const token = generateToken();
const matchedText = args[0];

matches[token] = new matcher.Match({
tagBuilder,
matchedText,
offset: args[args.length - 2],
[matcher.id as MatcherId]: matchedText,
});

return token;
});
}
});

// User-specified custom matchers
matchers.forEach((matcher) => {
linkedText = linkedText.replace(matcher.pattern, (...replacerArgs) => {
Expand Down
52 changes: 0 additions & 52 deletions src/matchers.ts

This file was deleted.

18 changes: 18 additions & 0 deletions src/matchers/__tests__/location.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*!
* React Native Autolink
*
* Copyright 2016-2021 Josh Swan
* Released under the MIT license
* https://github.com/joshswan/react-native-autolink/blob/master/LICENSE
*/

import { LatLngMatcher } from '..';

describe('Location matchers', () => {
describe('LatLngMatcher', () => {
test('matches latitude/longitude pair in text', () => {
const text = 'Location is 34.0522, -118.2437.';
expect(text.replace(LatLngMatcher.pattern, 'MATCH')).toBe('Location is MATCH.');
});
});
});
50 changes: 50 additions & 0 deletions src/matchers/__tests__/phone.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*!
* React Native Autolink
*
* Copyright 2016-2021 Josh Swan
* Released under the MIT license
* https://github.com/joshswan/react-native-autolink/blob/master/LICENSE
*/

import { IntlPhoneMatcher, PhoneMatchersByCountry } from '..';

const getText = (number: string) => `Phone number is ${number}.`;
const resultText = 'Phone number is MATCH.';

describe('Phone matchers', () => {
describe('Generic', () => {
test('matches common international phone numbers', () => {
expect(getText('+12317527630').replace(IntlPhoneMatcher.pattern, 'MATCH')).toBe(resultText);
expect(getText('+4915789173959').replace(IntlPhoneMatcher.pattern, 'MATCH')).toBe(resultText);
expect(getText('+33699520828').replace(IntlPhoneMatcher.pattern, 'MATCH')).toBe(resultText);
expect(getText('+48571775914').replace(IntlPhoneMatcher.pattern, 'MATCH')).toBe(resultText);
expect(getText('+447487428082').replace(IntlPhoneMatcher.pattern, 'MATCH')).toBe(resultText);
});
});

describe('PhoneMatchersByCountry', () => {
test('matches French phone numbers', () => {
expect(getText('+33699520828').replace(PhoneMatchersByCountry.FR.pattern, 'MATCH')).toBe(
resultText,
);
});

test('matches Polish phone numbers', () => {
expect(getText('+48571775914').replace(PhoneMatchersByCountry.PL.pattern, 'MATCH')).toBe(
resultText,
);
});

test('matches UK phone numbers', () => {
expect(getText('+447487428082').replace(PhoneMatchersByCountry.UK.pattern, 'MATCH')).toBe(
resultText,
);
});

test('matches US phone numbers', () => {
expect(getText('+12317527630').replace(PhoneMatchersByCountry.US.pattern, 'MATCH')).toBe(
resultText,
);
});
});
});
10 changes: 10 additions & 0 deletions src/matchers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*!
* React Native Autolink
*
* Copyright 2016-2021 Josh Swan
* Released under the MIT license
* https://github.com/joshswan/react-native-autolink/blob/master/LICENSE
*/

export * from './location';
export * from './phone';
21 changes: 21 additions & 0 deletions src/matchers/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*!
* React Native Autolink
*
* Copyright 2016-2021 Josh Swan
* Released under the MIT license
* https://github.com/joshswan/react-native-autolink/blob/master/LICENSE
*/

import { Platform } from 'react-native';
import type { CustomMatcher } from '../CustomMatch';

export const LatLngMatcher: CustomMatcher = {
pattern: /[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)/g,
type: 'latlng',
getLinkUrl: ([latlng]) => {
const query = latlng.replace(/\s/g, '');
return Platform.OS === 'ios' || Platform.OS === 'macos'
? `http://maps.apple.com/?q=${encodeURIComponent(latlng)}&ll=${query}`
: `https://www.google.com/maps/search/?api=1&query=${query}`;
},
};
39 changes: 39 additions & 0 deletions src/matchers/phone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*!
* React Native Autolink
*
* Copyright 2016-2021 Josh Swan
* Released under the MIT license
* https://github.com/joshswan/react-native-autolink/blob/master/LICENSE
*/

import type { CustomMatcher } from '../CustomMatch';

export const IntlPhoneMatcher: CustomMatcher = {
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,
type: 'phone-intl',
getLinkUrl: ([number]) => `tel:${number}`,
};

// NOTE: These patterns don't support extensions (i.e. "x" or "ext")
const patternsByCountry = {
// France
FR: /(\+33|0|0033)[1-9]\d{8}/g,
// Poland
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,
// United Kingdom
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,
// United States
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,
};

export const PhoneMatchersByCountry = Object.entries(patternsByCountry).reduce(
(matchers, [countryCode, pattern]) => ({
...matchers,
[countryCode]: {
pattern,
type: `phone-${countryCode}`,
getLinkUrl: ([number]) => `tel:${number.replace(/[^\d+]/g, '')}`,
} as CustomMatcher,
}),
{} as Record<keyof typeof patternsByCountry, CustomMatcher>,
);

0 comments on commit d229141

Please sign in to comment.