2022-07-04 18:10:14 +00:00
import React , { PureComponent } from 'react' ;
2022-02-17 15:27:01 +00:00
import { Image , StyleProp , Text , TextStyle } from 'react-native' ;
2022-05-10 17:40:08 +00:00
import { Parser } from 'commonmark' ;
2019-08-27 12:25:38 +00:00
import Renderer from 'commonmark-react-renderer' ;
2021-10-20 16:32:58 +00:00
import { MarkdownAST } from '@rocket.chat/message-parser' ;
2019-08-27 12:25:38 +00:00
import MarkdownLink from './Link' ;
import MarkdownList from './List' ;
import MarkdownListItem from './ListItem' ;
import MarkdownAtMention from './AtMention' ;
import MarkdownHashtag from './Hashtag' ;
import MarkdownBlockQuote from './BlockQuote' ;
import MarkdownEmoji from './Emoji' ;
import MarkdownTable from './Table' ;
2022-07-04 18:10:14 +00:00
import MarkdownTableRow from './TableRow' ;
import MarkdownTableCell from './TableCell' ;
2020-02-28 16:18:03 +00:00
import mergeTextNodes from './mergeTextNodes' ;
2019-08-27 12:25:38 +00:00
import styles from './styles' ;
2022-07-04 18:10:14 +00:00
import { isValidURL } from '../../lib/methods/helpers/url' ;
2021-10-20 16:32:58 +00:00
import NewMarkdown from './new' ;
2022-02-17 15:27:01 +00:00
import { formatText } from './formatText' ;
import { IUserMention , IUserChannel , TOnLinkPress } from './interfaces' ;
2022-07-04 18:10:14 +00:00
import { TGetCustomEmoji } from '../../definitions/IEmoji' ;
2022-02-17 15:27:01 +00:00
import { formatHyperlink } from './formatHyperlink' ;
2022-08-11 14:50:03 +00:00
import { TSupportedThemes , withTheme } from '../../theme' ;
2022-07-04 18:10:14 +00:00
import { themes } from '../../lib/constants' ;
2022-02-17 15:27:01 +00:00
export { default as MarkdownPreview } from './Preview' ;
2021-10-20 16:32:58 +00:00
2022-07-04 18:10:14 +00:00
interface IMarkdownProps {
2022-04-14 20:30:41 +00:00
msg? : string | null ;
2022-08-11 14:50:03 +00:00
theme? : TSupportedThemes ;
2022-02-17 15:27:01 +00:00
md? : MarkdownAST ;
mentions? : IUserMention [ ] ;
getCustomEmoji? : TGetCustomEmoji ;
baseUrl? : string ;
username? : string ;
tmid? : string ;
numberOfLines? : number ;
customEmojis? : boolean ;
useRealName? : boolean ;
channels? : IUserChannel [ ] ;
enableMessageParser? : boolean ;
2022-07-04 18:10:14 +00:00
// TODO: Refactor when migrate Room
navToRoomInfo? : Function ;
2022-02-17 15:27:01 +00:00
testID? : string ;
style? : StyleProp < TextStyle > [ ] ;
onLinkPress? : TOnLinkPress ;
2021-09-13 20:41:05 +00:00
}
type TLiteral = {
literal : string ;
} ;
2019-08-27 12:25:38 +00:00
const emojiRanges = [
'\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]' , // unicode emoji from https://www.regextester.com/106421
':.{1,40}:' , // custom emoji
' |\n' // allow spaces and line breaks
] . join ( '|' ) ;
2021-09-13 20:41:05 +00:00
const removeSpaces = ( str : string ) = > str && str . replace ( /\s/g , '' ) ;
2019-11-27 20:53:14 +00:00
2021-09-13 20:41:05 +00:00
const removeAllEmoji = ( str : string ) = > str . replace ( new RegExp ( emojiRanges , 'g' ) , '' ) ;
2019-08-27 12:25:38 +00:00
2021-09-13 20:41:05 +00:00
const isOnlyEmoji = ( str : string ) = > {
2019-11-27 20:53:14 +00:00
str = removeSpaces ( str ) ;
return ! removeAllEmoji ( str ) . length ;
} ;
2019-08-27 12:25:38 +00:00
2021-09-13 20:41:05 +00:00
const removeOneEmoji = ( str : string ) = > str . replace ( new RegExp ( emojiRanges ) , '' ) ;
2019-08-27 12:25:38 +00:00
2021-09-13 20:41:05 +00:00
const emojiCount = ( str : string ) = > {
2019-11-27 20:53:14 +00:00
str = removeSpaces ( str ) ;
2019-08-27 12:25:38 +00:00
let oldLength = 0 ;
let counter = 0 ;
while ( oldLength !== str . length ) {
oldLength = str . length ;
str = removeOneEmoji ( str ) ;
if ( oldLength !== str . length ) {
counter += 1 ;
}
}
return counter ;
} ;
2020-02-17 19:06:18 +00:00
const parser = new Parser ( ) ;
2022-07-04 18:10:14 +00:00
class Markdown extends PureComponent < IMarkdownProps , any > {
private renderer : any ;
private isMessageContainsOnlyEmoji ! : boolean ;
constructor ( props : IMarkdownProps ) {
super ( props ) ;
if ( ! this . isNewMarkdown ) {
this . renderer = this . createRenderer ( ) ;
2021-10-20 16:32:58 +00:00
}
2022-07-04 18:10:14 +00:00
}
2019-08-27 12:25:38 +00:00
2022-07-04 18:10:14 +00:00
createRenderer = ( ) = >
new Renderer ( {
renderers : {
text : this.renderText ,
emph : Renderer.forwardChildren ,
strong : Renderer.forwardChildren ,
del : Renderer.forwardChildren ,
code : this.renderCodeInline ,
link : this.renderLink ,
image : this.renderImage ,
atMention : this.renderAtMention ,
emoji : this.renderEmoji ,
hashtag : this.renderHashtag ,
paragraph : this.renderParagraph ,
heading : this.renderHeading ,
codeBlock : this.renderCodeBlock ,
blockQuote : this.renderBlockQuote ,
list : this.renderList ,
item : this.renderListItem ,
hardBreak : this.renderBreak ,
thematicBreak : this.renderBreak ,
softBreak : this.renderBreak ,
htmlBlock : this.renderText ,
htmlInline : this.renderText ,
table : this.renderTable ,
table_row : this.renderTableRow ,
table_cell : this.renderTableCell
} ,
renderParagraphsInLists : true
} ) ;
get isNewMarkdown ( ) : boolean {
const { md , enableMessageParser } = this . props ;
2022-07-20 21:02:18 +00:00
return ( enableMessageParser ? ? true ) && ! ! md ;
2021-10-20 16:32:58 +00:00
}
2022-07-04 18:10:14 +00:00
renderText = ( { context , literal } : { context : [ ] ; literal : string } ) = > {
const { numberOfLines , style = [ ] } = this . props ;
const defaultStyle = [ this . isMessageContainsOnlyEmoji ? styles . textBig : { } , . . . context . map ( type = > styles [ type ] ) ] ;
return (
< Text accessibilityLabel = { literal } style = { [ styles . text , defaultStyle , . . . style ] } numberOfLines = { numberOfLines } >
{ literal }
< / Text >
) ;
} ;
renderCodeInline = ( { literal } : TLiteral ) = > {
const { theme , style = [ ] } = this . props ;
return (
< Text
style = { [
{
. . . styles . codeInline ,
2022-08-11 14:50:03 +00:00
color : themes [ theme ! ] . bodyText ,
backgroundColor : themes [ theme ! ] . bannerBackground ,
borderColor : themes [ theme ! ] . bannerBackground
2022-07-04 18:10:14 +00:00
} ,
. . . style
2022-08-08 21:02:08 +00:00
] }
>
2022-07-04 18:10:14 +00:00
{ literal }
< / Text >
) ;
} ;
renderCodeBlock = ( { literal } : TLiteral ) = > {
const { theme , style = [ ] } = this . props ;
2019-12-04 16:39:53 +00:00
return (
2022-07-04 18:10:14 +00:00
< Text
style = { [
{
. . . styles . codeBlock ,
2022-08-11 14:50:03 +00:00
color : themes [ theme ! ] . bodyText ,
backgroundColor : themes [ theme ! ] . bannerBackground ,
borderColor : themes [ theme ! ] . bannerBackground
2022-07-04 18:10:14 +00:00
} ,
. . . style
2022-08-08 21:02:08 +00:00
] }
>
2019-12-04 16:39:53 +00:00
{ literal }
< / Text >
) ;
2019-10-02 12:41:51 +00:00
} ;
2019-08-27 12:25:38 +00:00
2022-07-04 18:10:14 +00:00
renderBreak = ( ) = > {
const { tmid } = this . props ;
return < Text > { tmid ? ' ' : '\n' } < / Text > ;
} ;
renderParagraph = ( { children } : any ) = > {
const { numberOfLines , style , theme } = this . props ;
2019-08-27 12:25:38 +00:00
if ( ! children || children . length === 0 ) {
return null ;
}
return (
2022-08-11 14:50:03 +00:00
< Text style = { [ styles . text , style , { color : themes [ theme ! ] . bodyText } ] } numberOfLines = { numberOfLines } >
2019-10-02 12:41:51 +00:00
{ children }
< / Text >
2019-08-27 12:25:38 +00:00
) ;
} ;
2022-07-04 18:10:14 +00:00
renderLink = ( { children , href } : any ) = > {
const { theme , onLinkPress } = this . props ;
return (
2022-08-11 14:50:03 +00:00
< MarkdownLink link = { href } theme = { theme ! } onLinkPress = { onLinkPress } >
2022-07-04 18:10:14 +00:00
{ children }
< / MarkdownLink >
) ;
} ;
renderHashtag = ( { hashtag } : { hashtag : string } ) = > {
const { channels , navToRoomInfo , style } = this . props ;
return < MarkdownHashtag hashtag = { hashtag } channels = { channels } navToRoomInfo = { navToRoomInfo } style = { style } / > ;
} ;
renderAtMention = ( { mentionName } : { mentionName : string } ) = > {
const { username = '' , mentions , navToRoomInfo , useRealName , style } = this . props ;
return (
< MarkdownAtMention
mentions = { mentions }
mention = { mentionName }
useRealName = { useRealName }
username = { username }
navToRoomInfo = { navToRoomInfo }
style = { style }
/ >
) ;
} ;
renderEmoji = ( { literal } : TLiteral ) = > {
const { getCustomEmoji , baseUrl = '' , customEmojis , style } = this . props ;
return (
< MarkdownEmoji
literal = { literal }
isMessageContainsOnlyEmoji = { this . isMessageContainsOnlyEmoji }
getCustomEmoji = { getCustomEmoji }
baseUrl = { baseUrl }
customEmojis = { customEmojis }
style = { style }
/ >
) ;
} ;
renderImage = ( { src } : { src : string } ) = > {
2020-03-20 16:26:50 +00:00
if ( ! isValidURL ( src ) ) {
return null ;
}
2022-07-04 18:10:14 +00:00
return < Image style = { styles . inlineImage } source = { { uri : encodeURI ( src ) } } / > ;
2021-09-13 20:41:05 +00:00
} ;
2019-08-27 12:25:38 +00:00
2022-07-04 18:10:14 +00:00
renderHeading = ( { children , level } : any ) = > {
const { numberOfLines , theme } = this . props ;
2022-03-21 20:44:06 +00:00
// @ts-ignore
2021-09-13 20:41:05 +00:00
const textStyle = styles [ ` heading ${ level } Text ` ] ;
2019-08-27 12:25:38 +00:00
return (
2022-08-11 14:50:03 +00:00
< Text numberOfLines = { numberOfLines } style = { [ textStyle , { color : themes [ theme ! ] . bodyText } ] } >
2019-08-27 12:25:38 +00:00
{ children }
< / Text >
) ;
} ;
2022-07-04 18:10:14 +00:00
renderList = ( { children , start , tight , type } : any ) = > {
const { numberOfLines } = this . props ;
return (
< MarkdownList ordered = { type !== 'bullet' } start = { start } tight = { tight } numberOfLines = { numberOfLines } >
{ children }
< / MarkdownList >
) ;
} ;
2019-08-27 12:25:38 +00:00
2022-07-04 18:10:14 +00:00
renderListItem = ( { children , context , . . . otherProps } : any ) = > {
const { theme } = this . props ;
2021-09-13 20:41:05 +00:00
const level = context . filter ( ( type : string ) = > type === 'list' ) . length ;
2019-08-27 12:25:38 +00:00
return (
2022-07-04 18:10:14 +00:00
< MarkdownListItem level = { level } theme = { theme } { ...otherProps } >
2019-08-27 12:25:38 +00:00
{ children }
< / MarkdownListItem >
) ;
} ;
2022-07-04 18:10:14 +00:00
renderBlockQuote = ( { children } : { children : JSX.Element } ) = > {
const { theme } = this . props ;
2022-08-11 14:50:03 +00:00
return < MarkdownBlockQuote theme = { theme ! } > { children } < / MarkdownBlockQuote > ;
2022-07-04 18:10:14 +00:00
} ;
renderTable = ( { children , numColumns } : { children : JSX.Element ; numColumns : number } ) = > {
const { theme } = this . props ;
return (
2022-08-11 14:50:03 +00:00
< MarkdownTable numColumns = { numColumns } theme = { theme ! } >
2022-07-04 18:10:14 +00:00
{ children }
< / MarkdownTable >
) ;
} ;
renderTableRow = ( args : any ) = > {
const { theme } = this . props ;
return < MarkdownTableRow { ...args } theme = { theme } / > ;
} ;
2019-08-27 12:25:38 +00:00
2022-07-04 18:10:14 +00:00
renderTableCell = ( args : any ) = > {
const { theme } = this . props ;
return < MarkdownTableCell { ...args } theme = { theme } / > ;
} ;
2019-08-27 12:25:38 +00:00
2022-07-04 18:10:14 +00:00
render() {
const {
msg ,
md ,
mentions ,
channels ,
navToRoomInfo ,
useRealName ,
username = '' ,
getCustomEmoji ,
baseUrl = '' ,
onLinkPress
} = this . props ;
if ( ! msg ) {
return null ;
}
2019-08-27 12:25:38 +00:00
2022-07-04 18:10:14 +00:00
if ( this . isNewMarkdown ) {
return (
< NewMarkdown
username = { username }
baseUrl = { baseUrl }
getCustomEmoji = { getCustomEmoji }
useRealName = { useRealName }
tokens = { md }
mentions = { mentions }
channels = { channels }
navToRoomInfo = { navToRoomInfo }
onLinkPress = { onLinkPress }
/ >
) ;
}
2021-10-20 16:32:58 +00:00
2022-07-04 18:10:14 +00:00
let m = formatText ( msg ) ;
m = formatHyperlink ( m ) ;
let ast = parser . parse ( m ) ;
ast = mergeTextNodes ( ast ) ;
this . isMessageContainsOnlyEmoji = isOnlyEmoji ( m ) && emojiCount ( m ) <= 3 ;
2022-09-28 20:06:43 +00:00
return this . renderer ? . render ( ast ) || null ;
2019-08-27 12:25:38 +00:00
}
2022-07-04 18:10:14 +00:00
}
2019-12-04 16:39:53 +00:00
2022-08-11 14:50:03 +00:00
export default withTheme ( Markdown ) ;