@@ -22,7 +22,7 @@ import { getEmojiFromUnicode } from "@matrix-org/emojibase-bindings";
2222import SettingsStore from "./settings/SettingsStore" ;
2323import { stripHTMLReply , stripPlainReply } from "./utils/Reply" ;
2424import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils" ;
25- import { sanitizeHtmlParams , transformTags } from "./Linkify" ;
25+ import { linkifyHtml , sanitizeHtmlParams , transformTags } from "./Linkify" ;
2626import { graphemeSegmenter } from "./utils/strings" ;
2727
2828export { Linkify , linkifyAndSanitizeHtml } from "./Linkify" ;
@@ -298,6 +298,7 @@ export interface EventRenderOpts {
298298 * Should inline media be rendered?
299299 */
300300 mediaIsVisible ?: boolean ;
301+ linkify ?: boolean ;
301302}
302303
303304function analyseEvent ( content : IContent , highlights : Optional < string [ ] > , opts : EventRenderOpts = { } ) : EventAnalysis {
@@ -320,6 +321,18 @@ function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: E
320321 } ;
321322 }
322323
324+ if ( opts . linkify ) {
325+ // Prevent mutating the source of sanitizeParams.
326+ sanitizeParams = { ...sanitizeParams } ;
327+ sanitizeParams . allowedClasses ??= { } ;
328+ if ( typeof sanitizeParams . allowedClasses . a === "boolean" ) {
329+ // All classes are already allowed for "a"
330+ } else {
331+ sanitizeParams . allowedClasses . a ??= [ ] ;
332+ sanitizeParams . allowedClasses . a . push ( "linkified" ) ;
333+ }
334+ }
335+
323336 try {
324337 const isFormattedBody =
325338 content . format === "org.matrix.custom.html" && typeof content . formatted_body === "string" ;
@@ -346,19 +359,26 @@ function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: E
346359 ? new HtmlHighlighter ( "mx_EventTile_searchHighlight" , opts . highlightLink )
347360 : null ;
348361
362+ if ( highlighter ) {
363+ // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
364+ // to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
365+ // are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
366+ // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
367+ // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
368+ sanitizeParams . textFilter = function ( safeText ) {
369+ return highlighter . applyHighlights ( safeText , safeHighlights ! ) . join ( "" ) ;
370+ } ;
371+ }
372+
349373 if ( isFormattedBody ) {
350- if ( highlighter ) {
351- // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
352- // to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
353- // are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
354- // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
355- // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
356- sanitizeParams . textFilter = function ( safeText ) {
357- return highlighter . applyHighlights ( safeText , safeHighlights ! ) . join ( "" ) ;
358- } ;
374+ let unsafeBody = formattedBody ! ;
375+
376+ if ( opts . linkify ) {
377+ unsafeBody = linkifyHtml ( unsafeBody ) ;
359378 }
360379
361- safeBody = sanitizeHtml ( formattedBody ! , sanitizeParams ) ;
380+ safeBody = sanitizeHtml ( unsafeBody , sanitizeParams ) ;
381+
362382 const phtml = new DOMParser ( ) . parseFromString ( safeBody , "text/html" ) ;
363383 const isPlainText = phtml . body . innerHTML === phtml . body . textContent ;
364384 isHtmlMessage = ! isPlainText ;
@@ -373,6 +393,9 @@ function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: E
373393 } ) ;
374394 safeBody = phtml . body . innerHTML ;
375395 }
396+ } else if ( opts . linkify ) {
397+ // If we are linkifying plain text, pass the result through sanitizeHtml so that the highlighter configured in sanitizeParams.textFilter gets applied.
398+ safeBody = sanitizeHtml ( linkifyHtml ( escapeHtml ( plainBody ) ) , sanitizeParams ) ;
376399 } else if ( highlighter ) {
377400 safeBody = highlighter . applyHighlights ( escapeHtml ( plainBody ) , safeHighlights ! ) . join ( "" ) ;
378401 }
@@ -428,14 +451,15 @@ export function bodyToNode(
428451 } ) ;
429452
430453 let formattedBody = eventInfo . safeBody ;
431- if ( eventInfo . isFormattedBody && eventInfo . bodyHasEmoji && eventInfo . safeBody ) {
432- // This has to be done after the emojiBody check as to not break big emoji on replies
433- formattedBody = formatEmojis ( eventInfo . safeBody , true ) . join ( "" ) ;
434- }
435-
436454 let emojiBodyElements : JSX . Element [ ] | undefined ;
437- if ( ! eventInfo . safeBody && eventInfo . bodyHasEmoji ) {
438- emojiBodyElements = formatEmojis ( eventInfo . strippedBody , false ) as JSX . Element [ ] ;
455+
456+ if ( eventInfo . bodyHasEmoji ) {
457+ if ( eventInfo . safeBody ) {
458+ // This has to be done after the emojiBody check as to not break big emoji on replies
459+ formattedBody = formatEmojis ( eventInfo . safeBody , true ) . join ( "" ) ;
460+ } else {
461+ emojiBodyElements = formatEmojis ( eventInfo . strippedBody , false ) as JSX . Element [ ] ;
462+ }
439463 }
440464
441465 return { strippedBody : eventInfo . strippedBody , formattedBody, emojiBodyElements, className } ;
@@ -458,7 +482,7 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
458482 const eventInfo = analyseEvent ( content , highlights , opts ) ;
459483
460484 let formattedBody = eventInfo . safeBody ;
461- if ( eventInfo . isFormattedBody && eventInfo . bodyHasEmoji && formattedBody ) {
485+ if ( eventInfo . bodyHasEmoji && formattedBody ) {
462486 // This has to be done after the emojiBody check above as to not break big emoji on replies
463487 formattedBody = formatEmojis ( eventInfo . safeBody , true ) . join ( "" ) ;
464488 }
0 commit comments