2018-05-18 21:48:20 +00:00
|
|
|
import React from 'react';
|
|
|
|
|
2018-05-22 19:31:43 +00:00
|
|
|
import LinkifyIt from 'linkify-it';
|
2018-05-18 21:48:20 +00:00
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
import { RenderTextCallbackType } from '../../types/Util';
|
2019-02-21 20:28:13 +00:00
|
|
|
import { isLinkSneaky } from '../../../js/modules/link_previews';
|
2018-05-18 21:48:20 +00:00
|
|
|
|
2018-05-22 19:31:43 +00:00
|
|
|
const linkify = LinkifyIt();
|
2018-05-18 21:48:20 +00:00
|
|
|
|
2020-08-23 19:32:24 +00:00
|
|
|
export interface Props {
|
2018-05-18 21:48:20 +00:00
|
|
|
text: string;
|
|
|
|
/** Allows you to customize now non-links are rendered. Simplest is just a <span>. */
|
2019-01-14 21:49:58 +00:00
|
|
|
renderNonLink?: RenderTextCallbackType;
|
2018-05-18 21:48:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const SUPPORTED_PROTOCOLS = /^(http|https):/i;
|
|
|
|
|
2018-05-22 19:31:43 +00:00
|
|
|
export class Linkify extends React.Component<Props> {
|
2018-05-18 21:48:20 +00:00
|
|
|
public static defaultProps: Partial<Props> = {
|
2018-06-27 20:53:49 +00:00
|
|
|
renderNonLink: ({ text }) => text,
|
2018-05-18 21:48:20 +00:00
|
|
|
};
|
|
|
|
|
2020-09-14 19:51:27 +00:00
|
|
|
public render():
|
|
|
|
| JSX.Element
|
|
|
|
| string
|
|
|
|
| null
|
|
|
|
| Array<JSX.Element | string | null> {
|
2018-05-18 21:48:20 +00:00
|
|
|
const { text, renderNonLink } = this.props;
|
|
|
|
const matchData = linkify.match(text) || [];
|
2020-09-14 19:51:27 +00:00
|
|
|
const results: Array<JSX.Element | string> = [];
|
2018-05-18 21:48:20 +00:00
|
|
|
let last = 0;
|
|
|
|
let count = 1;
|
|
|
|
|
|
|
|
// We have to do this, because renderNonLink is not required in our Props object,
|
|
|
|
// but it is always provided via defaultProps.
|
|
|
|
if (!renderNonLink) {
|
2020-09-14 19:51:27 +00:00
|
|
|
return null;
|
2018-05-18 21:48:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (matchData.length === 0) {
|
|
|
|
return renderNonLink({ text, key: 0 });
|
|
|
|
}
|
|
|
|
|
|
|
|
matchData.forEach(
|
|
|
|
(match: {
|
|
|
|
index: number;
|
|
|
|
url: string;
|
|
|
|
lastIndex: number;
|
|
|
|
text: string;
|
|
|
|
}) => {
|
|
|
|
if (last < match.index) {
|
|
|
|
const textWithNoLink = text.slice(last, match.index);
|
2020-09-14 19:51:27 +00:00
|
|
|
count += 1;
|
|
|
|
results.push(renderNonLink({ text: textWithNoLink, key: count }));
|
2018-05-18 21:48:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const { url, text: originalText } = match;
|
2020-09-14 19:51:27 +00:00
|
|
|
count += 1;
|
2020-04-24 16:57:04 +00:00
|
|
|
if (SUPPORTED_PROTOCOLS.test(url) && !isLinkSneaky(url)) {
|
2018-05-18 21:48:20 +00:00
|
|
|
results.push(
|
2020-09-14 19:51:27 +00:00
|
|
|
<a key={count} href={url}>
|
2018-05-18 21:48:20 +00:00
|
|
|
{originalText}
|
|
|
|
</a>
|
|
|
|
);
|
|
|
|
} else {
|
2020-09-14 19:51:27 +00:00
|
|
|
results.push(renderNonLink({ text: originalText, key: count }));
|
2018-05-18 21:48:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
last = match.lastIndex;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
if (last < text.length) {
|
2020-09-14 19:51:27 +00:00
|
|
|
count += 1;
|
|
|
|
results.push(renderNonLink({ text: text.slice(last), key: count }));
|
2018-05-18 21:48:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
}
|