@@ -7,52 +7,121 @@ const messageMarkdown = (text: string) => {
77
88 let output = text
99
10- const markdownLinkRegex =
11- / \[ ( [ ^ \] ] + ?) ] \( \s * ( [ ^ ) \s ] + ?) (?: \s + [ " ' ] ( [ ^ " ' ] + ) [ " ' ] ) ? \s * \) / g
12- output = output . replace ( markdownLinkRegex , ( match , linkText , url , title ) => {
13- const href = url . trim ( )
14- const titleAttribute = title ? ` title="${ title . replace ( / " / g, '"' ) } "` : ''
15- const escapedLinkText = linkText . replace ( / < / g, '<' ) . replace ( / > / g, '>' )
16- return `<a href="${ href } " target="_blank" rel="noopener noreferrer"${ titleAttribute } >${ escapedLinkText } </a>`
10+ // Process markdown links [text](url) first, before any HTML escaping
11+ const markdownLinkRegex = / \[ ( [ ^ \] ] + ) ] \( ( [ ^ ) ] + ) \) / g
12+ output = output . replace ( markdownLinkRegex , ( match , linkText , url ) => {
13+ const cleanUrl = url . trim ( )
14+ // Escape the link text to prevent HTML injection
15+ const escapedLinkText = linkText
16+ . replace ( / & / g, '&' )
17+ . replace ( / < / g, '<' )
18+ . replace ( / > / g, '>' )
19+ return `<a href="${ cleanUrl } " style="text-decoration:underline;" target="_blank" rel="noopener noreferrer">${ escapedLinkText } </a>`
1720 } )
1821
19- const potentialUrlRegex =
20- / ( (?: h t t p s ? : \/ \/ ) ? (?: [ \w - ] + \. ) + [ \w - ] + (?: [ \/ \? # ] [ ^ \s < > ( ) " ] * ) ? ) / gi
22+ // Find existing links to avoid double-processing
23+ const existingLinksRanges : { start : number ; end : number } [ ] = [ ]
24+ output . replace ( / < a \s [ ^ > ] * h r e f = " [ ^ " ] * " [ ^ > ] * > .* ?< \/ a > / gi, ( linkMatch , offset ) => {
25+ existingLinksRanges . push ( {
26+ start : offset ,
27+ end : offset + linkMatch . length ,
28+ } )
29+ return linkMatch
30+ } )
2131
32+ // Now escape HTML characters in text that is not part of links
33+ const segments : string [ ] = [ ]
2234 let lastIndex = 0
23- let processedOutput = ''
24- let match
2535
26- const existingLinksRanges : { start : number ; end : number } [ ] = [ ]
27- output . replace (
28- / < a \s [ ^ > ] * h r e f = " [ ^ " ] * " [ ^ > ] * > .* ?< \/ a > / g,
29- ( linkMatch , offset ) => {
30- existingLinksRanges . push ( {
31- start : offset ,
32- end : offset + linkMatch . length ,
33- } )
34- return linkMatch
35- }
36+ // Find all links
37+ output . replace ( / < a \s [ ^ > ] * h r e f = " [ ^ " ] * " [ ^ > ] * > .* ?< \/ a > / gi, ( linkMatch , offset ) => {
38+ // Add escaped text before this link
39+ const textBefore = output . substring ( lastIndex , offset )
40+ segments . push (
41+ textBefore
42+ . replace ( / & / g, '&' )
43+ . replace ( / < / g, '<' )
44+ . replace ( / > / g, '>' )
45+ )
46+
47+ // Add the link as-is (already processed)
48+ segments . push ( linkMatch )
49+
50+ lastIndex = offset + linkMatch . length
51+ return linkMatch
52+ } )
53+
54+ // Add remaining text (escaped)
55+ const remainingText = output . substring ( lastIndex )
56+ segments . push (
57+ remainingText
58+ . replace ( / & / g, '&' )
59+ . replace ( / < / g, '<' )
60+ . replace ( / > / g, '>' )
61+ )
62+
63+ output = segments . join ( '' )
64+
65+ // Process line breaks
66+ output = output . replace ( / ( \r \n | \n | \r ) / gm, '<br>' )
67+
68+ // Process bold (**text** or __text__)
69+ output = output . replace ( / ( \* \* | _ _ ) ( .* ?) \1/ g, '<strong>$2</strong>' )
70+
71+ // Process italics (*text* or _text_)
72+ output = output . replace ( / ( \* ) ( .* ?) \1/ g, '<em>$2</em>' )
73+
74+ // Process inline code (`code`)
75+ output = output . replace ( / ` ( [ ^ ` ] + ) ` / g, '<code>$1</code>' )
76+
77+ // Process strikethrough (~~text~~)
78+ output = output . replace ( / ~ ~ ( .* ?) ~ ~ / g, '<del>$1</del>' )
79+
80+ // Process hashtags (#tag)
81+ output = output . replace (
82+ / ( ^ | \s ) # ( [ ^ \s < ] + ) / gm,
83+ '$1<span class="hashtag">#$2</span>'
84+ )
85+
86+ // Process mentions (@user) - avoid email addresses
87+ output = output . replace (
88+ / ( ^ | \s ) @ ( [ ^ \s < @ ] + ) / gm,
89+ '$1<span class="mention">@$2</span>'
3690 )
3791
92+ // Process plain URLs, but avoid email addresses and existing links
93+ // Find existing links again after all processing
94+ const finalExistingLinksRanges : { start : number ; end : number } [ ] = [ ]
95+ output . replace ( / < a \s [ ^ > ] * h r e f = " [ ^ " ] * " [ ^ > ] * > .* ?< \/ a > / gi, ( linkMatch , offset ) => {
96+ finalExistingLinksRanges . push ( {
97+ start : offset ,
98+ end : offset + linkMatch . length ,
99+ } )
100+ return linkMatch
101+ } )
102+
103+ const potentialUrlRegex = / \b (?: h t t p s ? : \/ \/ | w w w \. ) [ \w - ] + (?: \. [ \w - ] + ) + (?: [ ^ \s < > ( ) ' " ] * [ ^ . \s < > ( ) ' " ] ) ? / gi
104+
105+ lastIndex = 0
106+ let processedOutput = ''
107+ let match
108+
38109 while ( ( match = potentialUrlRegex . exec ( output ) ) !== null ) {
39110 const matchStartIndex = match . index
40111 const matchEndIndex = matchStartIndex + match [ 0 ] . length
41112 const urlCandidate = match [ 0 ]
42113
114+ // Check if this URL is inside an existing link
43115 let isInsideExistingLink = false
44- for ( const range of existingLinksRanges ) {
116+ for ( const range of finalExistingLinksRanges ) {
45117 if ( matchStartIndex >= range . start && matchStartIndex < range . end ) {
46118 isInsideExistingLink = true
47119 break
48120 }
49121 }
50122
51- const textBefore = output . substring (
52- Math . max ( 0 , matchStartIndex - 7 ) ,
53- matchStartIndex
54- )
55- if ( textBefore . match ( / h r e f \s * = \s * [ " ' ] $ / i) ) {
123+ // Additional check to avoid email addresses
124+ if ( urlCandidate . includes ( '@' ) && ! urlCandidate . startsWith ( 'http' ) && ! urlCandidate . startsWith ( 'www' ) ) {
56125 isInsideExistingLink = true
57126 }
58127
@@ -62,41 +131,37 @@ const messageMarkdown = (text: string) => {
62131 processedOutput += urlCandidate
63132 } else {
64133 let urlToLink = urlCandidate
65- if (
66- ! urlToLink . startsWith ( 'http://' ) &&
67- ! urlToLink . startsWith ( 'https://' )
68- ) {
69- if ( / ^ ( [ \w - ] + \. ) + [ \w - ] + / . test ( urlToLink ) ) {
70- urlToLink = `https://${ urlToLink } `
71- } else {
72- processedOutput += urlCandidate
73- lastIndex = matchEndIndex
74- continue
75- }
134+ if ( ! urlToLink . startsWith ( 'http://' ) && ! urlToLink . startsWith ( 'https://' ) ) {
135+ urlToLink = `https://${ urlToLink } `
76136 }
77- processedOutput += `<a href="${ urlToLink } " target="_blank" rel="noopener noreferrer">${ urlCandidate } </a>`
137+ processedOutput += `<a href="${ urlToLink } " style="text-decoration:underline;" target="_blank" rel="noopener noreferrer">${ urlCandidate } </a>`
78138 }
79139 lastIndex = matchEndIndex
80140 }
81141 processedOutput += output . substring ( lastIndex )
82142 output = processedOutput
83143
84- output = output . replace ( / ( \r \n | \n | \r ) / gm, '<br>' )
85- output = output . replace ( / ( \* \* | _ _ ) ( .* ?) \1/ g, '<strong>$2</strong>' )
86- output = output . replace ( / ( \* | _ ) ( .* ?) \1/ g, '<em>$2</em>' )
87- output = output . replace ( / ` ( [ ^ ` ] + ) ` / g, '<code>$1</code>' )
88- output = output . replace ( / ~ ~ ( .* ?) ~ ~ / g, '<del>$1</del>' )
144+ const linkSegments : string [ ] = [ ]
145+ lastIndex = 0
89146
90- output = output . replace (
91- / ( ^ | \s ) # ( [ ^ \s < ] + ) / gm,
92- '$1<span class="hashtag">#$2</span>'
93- )
94- output = output . replace (
95- / ( ^ | \s ) @ ( [ ^ \s < ] + ) / gm,
96- '$1<span class="mention">@$2</span>'
97- )
147+ output . replace ( / < a \s [ ^ > ] * h r e f = " [ ^ " ] * " [ ^ > ] * > .* ?< \/ a > / gi, ( linkMatch , offset ) => {
148+ const textBefore = output . substring ( lastIndex , offset )
149+ const processedTextBefore = textBefore . replace ( / ( _ ) _ ( .* ?) _ \1/ g, '<em>$2</em>' )
150+ linkSegments . push ( processedTextBefore )
151+ linkSegments . push ( linkMatch )
152+
153+ lastIndex = offset + linkMatch . length
154+ return linkMatch
155+ } )
156+
157+ const remainingText2 = output . substring ( lastIndex )
158+
159+ const processedRemainingText = remainingText2 . replace ( / ( _ ) _ ( .* ?) _ \1/ g, '<em>$2</em>' )
160+ linkSegments . push ( processedRemainingText )
161+
162+ output = linkSegments . join ( '' )
98163
99164 return output
100165}
101166
102- export { messageMarkdown }
167+ export { messageMarkdown }
0 commit comments