diff --git a/package.json b/package.json index 2221477..6faa915 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@storybook/addons": "^6.1.21", "@storybook/react": "^6.1.21", "@types/d3": "^6.3.0", + "@types/linkify-it": "^3.0.2", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.2", "@types/react-virtualized": "^9.21.11", @@ -101,12 +102,12 @@ "@types/lodash": "^4.0.6", "@types/react-virtualized-auto-sizer": "^1.0.0", "@types/react-window": "^1.8.2", - "anchorme": "^2.1.2", "d3": "^6.6.0", "date-fns": "^2.19.0", "dompurify": "^2.2.9", "downshift": "^6.1.1", "immer": "^9.0.12", + "linkify-it": "^3.0.3", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "match-sorter": "^6.3.0", diff --git a/src/components/cell.tsx b/src/components/cell.tsx index 8eba132..8276273 100644 --- a/src/components/cell.tsx +++ b/src/components/cell.tsx @@ -1,12 +1,14 @@ import React, { useEffect } from 'react'; import { areEqual } from 'react-window'; import tw, { TwStyle } from 'twin.macro'; -import anchorme from 'anchorme'; +import Linkify from 'linkify-it'; import { cellTypeMap } from '../store'; import { DashIcon, DiffModifiedIcon, PlusIcon } from '@primer/octicons-react'; import DOMPurify from 'dompurify'; import { EditableCell } from './editable-cell'; +const linkify = Linkify().add('ftp:', null).add('mailto:', null); + interface CellProps { type: string; value: any; @@ -47,37 +49,39 @@ export const Cell = React.memo(function (props: CellProps) { onFocusChange, background, style = {}, - onMouseEnter = () => { }, + onMouseEnter = () => {}, } = props; // @ts-ignore const cellInfo = cellTypeMap[type]; - const { cell: CellComponent } = cellInfo || {} + const { cell: CellComponent } = cellInfo || {}; - const displayValue = (formattedValue || value || "").toString(); + const displayValue = (formattedValue || value || '').toString(); const isLongValue = (displayValue || '').length > 23; - const stringWithLinks = React.useMemo( - () => displayValue ? ( - DOMPurify.sanitize( - anchorme({ - input: displayValue + '', - options: { - attributes: { - target: '_blank', - rel: 'noopener', - }, - }, - }) - ) - ) : "", - [value] - ) + const stringWithLinks = React.useMemo(() => { + if (!displayValue) return ''; + + const sanitized = DOMPurify.sanitize(displayValue); + // Does the sanitized string contain any links? + if (!linkify.test(sanitized)) return sanitized; + + // If so, we need to linkify it. + const matches = linkify.match(sanitized); + + // If there are no matches, we can just return the sanitized string. + if (!matches || matches.length === 0) return sanitized; + + // Otherwise, let's naively use the first match. + return ` + ${matches[0].url} + `; + }, [value]); useEffect(() => { - if (!isFocused) return - onMouseEnter() - }, [isFocused]) + if (!isFocused) return; + onMouseEnter(); + }, [isFocused]); if (!cellInfo) return null; @@ -91,14 +95,15 @@ export const Cell = React.memo(function (props: CellProps) { 'modified-row': DiffModifiedIcon, }[status || '']; const statusColor = - isFirstColumn && - // @ts-ignore - { - new: 'text-green-400', - old: 'text-pink-400', - modified: 'text-yellow-500', - 'modified-row': 'text-yellow-500', - }[status || ''] || "" + (isFirstColumn && + // @ts-ignore + { + new: 'text-green-400', + old: 'text-pink-400', + modified: 'text-yellow-500', + 'modified-row': 'text-yellow-500', + }[status || '']) || + ''; return (