11import React , { useEffect } from 'react' ;
22import { areEqual } from 'react-window' ;
33import tw , { TwStyle } from 'twin.macro' ;
4- import anchorme from 'anchorme ' ;
4+ import Linkify from 'linkify-it ' ;
55import { cellTypeMap } from '../store' ;
66import { DashIcon , DiffModifiedIcon , PlusIcon } from '@primer/octicons-react' ;
77import DOMPurify from 'dompurify' ;
88import { EditableCell } from './editable-cell' ;
99
10+ const linkify = Linkify ( ) . add ( 'ftp:' , null ) . add ( 'mailto:' , null ) ;
11+
1012interface CellProps {
1113 type : string ;
1214 value : any ;
@@ -47,37 +49,39 @@ export const Cell = React.memo(function (props: CellProps) {
4749 onFocusChange,
4850 background,
4951 style = { } ,
50- onMouseEnter = ( ) => { } ,
52+ onMouseEnter = ( ) => { } ,
5153 } = props ;
5254
5355 // @ts -ignore
5456 const cellInfo = cellTypeMap [ type ] ;
5557
56- const { cell : CellComponent } = cellInfo || { }
58+ const { cell : CellComponent } = cellInfo || { } ;
5759
58- const displayValue = ( formattedValue || value || "" ) . toString ( ) ;
60+ const displayValue = ( formattedValue || value || '' ) . toString ( ) ;
5961 const isLongValue = ( displayValue || '' ) . length > 23 ;
60- const stringWithLinks = React . useMemo (
61- ( ) => displayValue ? (
62- DOMPurify . sanitize (
63- anchorme ( {
64- input : displayValue + '' ,
65- options : {
66- attributes : {
67- target : '_blank' ,
68- rel : 'noopener' ,
69- } ,
70- } ,
71- } )
72- )
73- ) : "" ,
74- [ value ]
75- )
62+ const stringWithLinks = React . useMemo ( ( ) => {
63+ if ( ! displayValue ) return '' ;
64+
65+ const sanitized = DOMPurify . sanitize ( displayValue ) ;
66+ // Does the sanitized string contain any links?
67+ if ( ! linkify . test ( sanitized ) ) return sanitized ;
68+
69+ // If so, we need to linkify it.
70+ const matches = linkify . match ( sanitized ) ;
71+
72+ // If there are no matches, we can just return the sanitized string.
73+ if ( ! matches || matches . length === 0 ) return sanitized ;
74+
75+ // Otherwise, let's naively use the first match.
76+ return `<a href="${ matches [ 0 ] . url } " target="_blank" rel="noopener">
77+ ${ matches [ 0 ] . url }
78+ </a>` ;
79+ } , [ value ] ) ;
7680
7781 useEffect ( ( ) => {
78- if ( ! isFocused ) return
79- onMouseEnter ( )
80- } , [ isFocused ] )
82+ if ( ! isFocused ) return ;
83+ onMouseEnter ( ) ;
84+ } , [ isFocused ] ) ;
8185
8286 if ( ! cellInfo ) return null ;
8387
@@ -91,14 +95,15 @@ export const Cell = React.memo(function (props: CellProps) {
9195 'modified-row' : DiffModifiedIcon ,
9296 } [ status || '' ] ;
9397 const statusColor =
94- isFirstColumn &&
95- // @ts -ignore
96- {
97- new : 'text-green-400' ,
98- old : 'text-pink-400' ,
99- modified : 'text-yellow-500' ,
100- 'modified-row' : 'text-yellow-500' ,
101- } [ status || '' ] || ""
98+ ( isFirstColumn &&
99+ // @ts -ignore
100+ {
101+ new : 'text-green-400' ,
102+ old : 'text-pink-400' ,
103+ modified : 'text-yellow-500' ,
104+ 'modified-row' : 'text-yellow-500' ,
105+ } [ status || '' ] ) ||
106+ '' ;
102107
103108 return (
104109 < div
@@ -109,12 +114,13 @@ export const Cell = React.memo(function (props: CellProps) {
109114 status === 'old' && tw `border-pink-200` ,
110115 status === 'modified' && tw `border-yellow-200` ,
111116 status === 'modified-row' && tw `border-gray-200` ,
112- ! status && tw `border-gray-200`
117+ ! status && tw `border-gray-200` ,
113118 ] }
114119 style = { {
115120 ...style ,
116121 background : background || '#fff' ,
117- } } >
122+ } }
123+ >
118124 < EditableCell
119125 value = { rawValue }
120126 isEditable = { isEditable }
@@ -123,7 +129,8 @@ export const Cell = React.memo(function (props: CellProps) {
123129 isFocused = { isFocused }
124130 isExtraBlankRow = { isExtraBlankRow }
125131 onFocusChange = { onFocusChange }
126- onRowDelete = { onRowDelete } >
132+ onRowDelete = { onRowDelete }
133+ >
127134 < CellInner
128135 value = { value }
129136 isFirstColumn = { isFirstColumn }
@@ -143,7 +150,6 @@ export const Cell = React.memo(function (props: CellProps) {
143150 ) ;
144151} , areEqual ) ;
145152
146-
147153const CellInner = React . memo ( function CellInner ( {
148154 value,
149155 isFirstColumn,
@@ -176,7 +182,7 @@ const CellInner = React.memo(function CellInner({
176182 css = { [
177183 tw `w-full h-full flex flex-none items-center px-4` ,
178184 typeof value === 'undefined' ||
179- ( Number . isNaN ( value ) && tw `text-gray-300` ) ,
185+ ( Number . isNaN ( value ) && tw `text-gray-300` ) ,
180186 ] }
181187 onMouseEnter = { ( ) => onMouseEnter ?.( ) }
182188 >
@@ -214,5 +220,5 @@ const CellInner = React.memo(function CellInner({
214220 </ div >
215221 ) }
216222 </ div >
217- )
218- } )
223+ ) ;
224+ } ) ;
0 commit comments