1
1
import React , { useEffect } from 'react' ;
2
2
import { areEqual } from 'react-window' ;
3
3
import tw , { TwStyle } from 'twin.macro' ;
4
- import anchorme from 'anchorme ' ;
4
+ import Linkify from 'linkify-it ' ;
5
5
import { cellTypeMap } from '../store' ;
6
6
import { DashIcon , DiffModifiedIcon , PlusIcon } from '@primer/octicons-react' ;
7
7
import DOMPurify from 'dompurify' ;
8
8
import { EditableCell } from './editable-cell' ;
9
9
10
+ const linkify = Linkify ( ) . add ( 'ftp:' , null ) . add ( 'mailto:' , null ) ;
11
+
10
12
interface CellProps {
11
13
type : string ;
12
14
value : any ;
@@ -47,37 +49,39 @@ export const Cell = React.memo(function (props: CellProps) {
47
49
onFocusChange,
48
50
background,
49
51
style = { } ,
50
- onMouseEnter = ( ) => { } ,
52
+ onMouseEnter = ( ) => { } ,
51
53
} = props ;
52
54
53
55
// @ts -ignore
54
56
const cellInfo = cellTypeMap [ type ] ;
55
57
56
- const { cell : CellComponent } = cellInfo || { }
58
+ const { cell : CellComponent } = cellInfo || { } ;
57
59
58
- const displayValue = ( formattedValue || value || "" ) . toString ( ) ;
60
+ const displayValue = ( formattedValue || value || '' ) . toString ( ) ;
59
61
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 ] ) ;
76
80
77
81
useEffect ( ( ) => {
78
- if ( ! isFocused ) return
79
- onMouseEnter ( )
80
- } , [ isFocused ] )
82
+ if ( ! isFocused ) return ;
83
+ onMouseEnter ( ) ;
84
+ } , [ isFocused ] ) ;
81
85
82
86
if ( ! cellInfo ) return null ;
83
87
@@ -91,14 +95,15 @@ export const Cell = React.memo(function (props: CellProps) {
91
95
'modified-row' : DiffModifiedIcon ,
92
96
} [ status || '' ] ;
93
97
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
+ '' ;
102
107
103
108
return (
104
109
< div
@@ -109,12 +114,13 @@ export const Cell = React.memo(function (props: CellProps) {
109
114
status === 'old' && tw `border-pink-200` ,
110
115
status === 'modified' && tw `border-yellow-200` ,
111
116
status === 'modified-row' && tw `border-gray-200` ,
112
- ! status && tw `border-gray-200`
117
+ ! status && tw `border-gray-200` ,
113
118
] }
114
119
style = { {
115
120
...style ,
116
121
background : background || '#fff' ,
117
- } } >
122
+ } }
123
+ >
118
124
< EditableCell
119
125
value = { rawValue }
120
126
isEditable = { isEditable }
@@ -123,7 +129,8 @@ export const Cell = React.memo(function (props: CellProps) {
123
129
isFocused = { isFocused }
124
130
isExtraBlankRow = { isExtraBlankRow }
125
131
onFocusChange = { onFocusChange }
126
- onRowDelete = { onRowDelete } >
132
+ onRowDelete = { onRowDelete }
133
+ >
127
134
< CellInner
128
135
value = { value }
129
136
isFirstColumn = { isFirstColumn }
@@ -143,7 +150,6 @@ export const Cell = React.memo(function (props: CellProps) {
143
150
) ;
144
151
} , areEqual ) ;
145
152
146
-
147
153
const CellInner = React . memo ( function CellInner ( {
148
154
value,
149
155
isFirstColumn,
@@ -176,7 +182,7 @@ const CellInner = React.memo(function CellInner({
176
182
css = { [
177
183
tw `w-full h-full flex flex-none items-center px-4` ,
178
184
typeof value === 'undefined' ||
179
- ( Number . isNaN ( value ) && tw `text-gray-300` ) ,
185
+ ( Number . isNaN ( value ) && tw `text-gray-300` ) ,
180
186
] }
181
187
onMouseEnter = { ( ) => onMouseEnter ?.( ) }
182
188
>
@@ -214,5 +220,5 @@ const CellInner = React.memo(function CellInner({
214
220
</ div >
215
221
) }
216
222
</ div >
217
- )
218
- } )
223
+ ) ;
224
+ } ) ;
0 commit comments