2019-08-27 12:25:38 +00:00
import React , { PureComponent } from 'react' ;
2019-10-02 12:41:51 +00:00
import { Text , Image } from 'react-native' ;
2019-08-27 12:25:38 +00:00
import { Parser , Node } from 'commonmark' ;
import Renderer from 'commonmark-react-renderer' ;
2020-02-28 16:18:03 +00:00
import removeMarkdown from 'remove-markdown' ;
2019-08-27 12:25:38 +00:00
2019-12-11 19:00:38 +00:00
import shortnameToUnicode from '../../utils/shortnameToUnicode' ;
2019-08-27 12:25:38 +00:00
import I18n from '../../i18n' ;
2019-12-04 16:39:53 +00:00
import { themes } from '../../constants/colors' ;
2019-08-27 12:25:38 +00:00
import MarkdownLink from './Link' ;
import MarkdownList from './List' ;
import MarkdownListItem from './ListItem' ;
import MarkdownAtMention from './AtMention' ;
2021-07-21 15:15:13 +00:00
import MarkdownHashtag , { TChannel } from './Hashtag' ;
2019-08-27 12:25:38 +00:00
import MarkdownBlockQuote from './BlockQuote' ;
import MarkdownEmoji from './Emoji' ;
import MarkdownTable from './Table' ;
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' ;
2020-03-20 16:26:50 +00:00
import { isValidURL } from '../../utils/url' ;
2019-08-27 12:25:38 +00:00
2021-07-21 15:15:13 +00:00
interface IMarkdownProps {
msg : string ;
getCustomEmoji : Function ;
baseUrl : string ;
username : string ;
tmid : string ;
isEdited : boolean ;
numberOfLines : number ;
customEmojis : boolean ;
useRealName : boolean ;
channels : TChannel [ ] ;
mentions : object [ ] ;
navToRoomInfo : Function ;
preview : boolean ;
theme : string ;
testID : string ;
style : any ;
onLinkPress : Function ;
}
type TLiteral = {
literal : string ;
}
2019-08-27 12:25:38 +00:00
// Support <http://link|Text>
2021-07-21 15:15:13 +00:00
const formatText = ( text : string ) = > text . replace (
2019-08-27 12:25:38 +00:00
new RegExp ( '(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)' , 'gm' ) ,
( match , url , title ) = > ` [ ${ title } ]( ${ url } ) `
) ;
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-07-21 15:15:13 +00:00
const removeSpaces = ( str : string ) = > str && str . replace ( /\s/g , '' ) ;
2019-11-27 20:53:14 +00:00
2021-07-21 15:15:13 +00:00
const removeAllEmoji = ( str : string ) = > str . replace ( new RegExp ( emojiRanges , 'g' ) , '' ) ;
2019-08-27 12:25:38 +00:00
2021-07-21 15:15:13 +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-07-21 15:15:13 +00:00
const removeOneEmoji = ( str : string ) = > str . replace ( new RegExp ( emojiRanges ) , '' ) ;
2019-08-27 12:25:38 +00:00
2021-07-21 15:15:13 +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 ( ) ;
2021-07-21 15:15:13 +00:00
class Markdown extends PureComponent < IMarkdownProps , any > {
private renderer : any ;
private isMessageContainsOnlyEmoji ! : boolean ;
2019-08-27 12:25:38 +00:00
2021-07-21 15:15:13 +00:00
constructor ( props : IMarkdownProps ) {
2019-08-27 12:25:38 +00:00
super ( props ) ;
2020-02-28 16:18:03 +00:00
this . renderer = this . createRenderer ( ) ;
2019-08-27 12:25:38 +00:00
}
2020-02-28 16:18:03 +00:00
createRenderer = ( ) = > new Renderer ( {
2019-08-27 12:25:38 +00:00
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 ,
2020-02-28 16:18:03 +00:00
editedIndicator : this.renderEditedIndicator
2019-08-27 12:25:38 +00:00
} ,
renderParagraphsInLists : true
} ) ;
2021-07-21 15:15:13 +00:00
editedMessage = ( ast : any ) = > {
2019-08-27 12:25:38 +00:00
const { isEdited } = this . props ;
if ( isEdited ) {
const editIndicatorNode = new Node ( 'edited_indicator' ) ;
if ( ast . lastChild && [ 'heading' , 'paragraph' ] . includes ( ast . lastChild . type ) ) {
ast . lastChild . appendChild ( editIndicatorNode ) ;
} else {
const node = new Node ( 'paragraph' ) ;
node . appendChild ( editIndicatorNode ) ;
ast . appendChild ( node ) ;
}
}
} ;
2021-07-21 15:15:13 +00:00
renderText = ( { context , literal } : { context : [ ] ; literal : string } ) = > {
2019-12-04 16:39:53 +00:00
const {
2020-02-28 16:18:03 +00:00
numberOfLines , style = [ ]
2019-12-04 16:39:53 +00:00
} = this . props ;
2019-10-02 12:41:51 +00:00
const defaultStyle = [
2020-02-28 16:18:03 +00:00
this . isMessageContainsOnlyEmoji ? styles . textBig : { } ,
2019-10-02 12:41:51 +00:00
. . . context . map ( type = > styles [ type ] )
] ;
2019-08-27 12:25:38 +00:00
return (
< Text
2020-03-03 20:27:38 +00:00
accessibilityLabel = { literal }
2020-02-28 16:18:03 +00:00
style = { [ styles . text , defaultStyle , . . . style ] }
2019-08-27 12:25:38 +00:00
numberOfLines = { numberOfLines }
>
{ literal }
< / Text >
) ;
}
2021-07-21 15:15:13 +00:00
renderCodeInline = ( { literal } : TLiteral ) = > {
2020-02-28 16:18:03 +00:00
const { theme , style = [ ] } = this . props ;
2019-12-04 16:39:53 +00:00
return (
< Text
style = { [
2020-02-28 16:18:03 +00:00
{
. . . styles . codeInline ,
color : themes [ theme ] . bodyText ,
backgroundColor : themes [ theme ] . bannerBackground ,
borderColor : themes [ theme ] . bannerBackground
} ,
2019-12-04 16:39:53 +00:00
. . . style
] }
>
{ literal }
< / Text >
) ;
2019-10-02 12:41:51 +00:00
} ;
2019-08-27 12:25:38 +00:00
2021-07-21 15:15:13 +00:00
renderCodeBlock = ( { literal } : TLiteral ) = > {
2020-02-28 16:18:03 +00:00
const { theme , style = [ ] } = this . props ;
2019-12-04 16:39:53 +00:00
return (
< Text
style = { [
2020-02-28 16:18:03 +00:00
{
. . . styles . codeBlock ,
color : themes [ theme ] . bodyText ,
backgroundColor : themes [ theme ] . bannerBackground ,
borderColor : themes [ theme ] . bannerBackground
} ,
2019-12-04 16:39:53 +00:00
. . . style
] }
>
{ literal }
< / Text >
) ;
2019-10-02 12:41:51 +00:00
} ;
2019-08-27 12:25:38 +00:00
renderBreak = ( ) = > {
const { tmid } = this . props ;
return < Text > { tmid ? ' ' : '\n' } < / Text > ;
}
2021-07-21 15:15:13 +00:00
renderParagraph = ( { children } : any ) = > {
2019-12-04 16:39:53 +00:00
const { numberOfLines , style , theme } = this . props ;
2019-08-27 12:25:38 +00:00
if ( ! children || children . length === 0 ) {
return null ;
}
return (
2020-11-30 21:47:05 +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
) ;
} ;
2021-07-21 15:15:13 +00:00
renderLink = ( { children , href } : any ) = > {
2021-05-26 17:24:54 +00:00
const { theme , onLinkPress } = this . props ;
2019-10-04 13:28:36 +00:00
return (
2019-12-04 16:39:53 +00:00
< MarkdownLink
link = { href }
theme = { theme }
2021-05-26 17:24:54 +00:00
onLinkPress = { onLinkPress }
2019-12-04 16:39:53 +00:00
>
2019-10-04 13:28:36 +00:00
{ children }
< / MarkdownLink >
) ;
}
2019-08-27 12:25:38 +00:00
2021-07-21 15:15:13 +00:00
renderHashtag = ( { hashtag } : { hashtag : string } ) = > {
const { channels , navToRoomInfo , style , theme } = this . props ;
2019-08-27 12:25:38 +00:00
return (
< MarkdownHashtag
hashtag = { hashtag }
channels = { channels }
navToRoomInfo = { navToRoomInfo }
2019-12-04 16:39:53 +00:00
theme = { theme }
2019-10-02 12:41:51 +00:00
style = { style }
2019-08-27 12:25:38 +00:00
/ >
) ;
}
2021-07-21 15:15:13 +00:00
renderAtMention = ( { mentionName } : { mentionName : string } ) = > {
const { username , mentions , navToRoomInfo , useRealName , style , theme } = this . props ;
2019-08-27 12:25:38 +00:00
return (
< MarkdownAtMention
mentions = { mentions }
mention = { mentionName }
2020-02-21 16:13:05 +00:00
useRealName = { useRealName }
2019-08-27 12:25:38 +00:00
username = { username }
navToRoomInfo = { navToRoomInfo }
2019-12-04 16:39:53 +00:00
theme = { theme }
2019-10-02 12:41:51 +00:00
style = { style }
2019-08-27 12:25:38 +00:00
/ >
) ;
}
2021-07-21 15:15:13 +00:00
renderEmoji = ( { literal } : TLiteral ) = > {
const { getCustomEmoji , baseUrl , customEmojis , style , theme } = this . props ;
2019-08-27 12:25:38 +00:00
return (
< MarkdownEmoji
literal = { literal }
2020-02-28 16:18:03 +00:00
isMessageContainsOnlyEmoji = { this . isMessageContainsOnlyEmoji }
2019-08-27 12:25:38 +00:00
getCustomEmoji = { getCustomEmoji }
baseUrl = { baseUrl }
2019-10-02 12:41:51 +00:00
customEmojis = { customEmojis }
style = { style }
2019-12-04 16:39:53 +00:00
theme = { theme }
2019-08-27 12:25:38 +00:00
/ >
) ;
}
2021-07-21 15:15:13 +00:00
renderImage = ( { src } : { src : string } ) = > {
2020-03-20 16:26:50 +00:00
if ( ! isValidURL ( src ) ) {
return null ;
}
return (
< Image
style = { styles . inlineImage }
source = { { uri : encodeURI ( src ) } }
/ >
) ;
}
2019-08-27 12:25:38 +00:00
2019-12-04 16:39:53 +00:00
renderEditedIndicator = ( ) = > {
const { theme } = this . props ;
return < Text style = { [ styles . edited , { color : themes [ theme ] . auxiliaryText } ] } > ( { I18n . t ( 'edited' ) } ) < / Text > ;
}
2019-08-27 12:25:38 +00:00
2021-07-21 15:15:13 +00:00
renderHeading = ( { children , level } : any ) = > {
2019-12-04 16:39:53 +00:00
const { numberOfLines , theme } = this . props ;
2019-08-27 12:25:38 +00:00
const textStyle = styles [ ` heading ${ level } Text ` ] ;
return (
2019-12-17 14:08:06 +00:00
< Text numberOfLines = { numberOfLines } style = { [ textStyle , { color : themes [ theme ] . bodyText } ] } >
2019-08-27 12:25:38 +00:00
{ children }
< / Text >
) ;
} ;
2021-07-21 15:15:13 +00:00
renderList = ( { children , start , tight , type } : any ) = > {
2019-10-02 12:41:51 +00:00
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
2021-07-21 15:15:13 +00:00
renderListItem = ( { children , context , . . . otherProps } : any ) = > {
2019-12-04 16:39:53 +00:00
const { theme } = this . props ;
2021-07-21 15:15:13 +00:00
const level = context . filter ( ( type : string ) = > type === 'list' ) . length ;
2019-08-27 12:25:38 +00:00
return (
< MarkdownListItem
level = { level }
2019-12-04 16:39:53 +00:00
theme = { theme }
2019-08-27 12:25:38 +00:00
{ . . . otherProps }
>
{ children }
< / MarkdownListItem >
) ;
} ;
2021-07-21 15:15:13 +00:00
renderBlockQuote = ( { children } : { children : JSX.Element } ) = > {
2020-02-28 16:18:03 +00:00
const { theme } = this . props ;
2019-10-04 13:28:36 +00:00
return (
2019-12-04 16:39:53 +00:00
< MarkdownBlockQuote theme = { theme } >
2019-10-04 13:28:36 +00:00
{ children }
< / MarkdownBlockQuote >
) ;
}
2019-08-27 12:25:38 +00:00
2021-07-21 15:15:13 +00:00
renderTable = ( { children , numColumns } : { children : JSX.Element ; numColumns : number } ) = > {
2019-12-04 16:39:53 +00:00
const { theme } = this . props ;
return (
< MarkdownTable numColumns = { numColumns } theme = { theme } >
{ children }
< / MarkdownTable >
) ;
}
2019-08-27 12:25:38 +00:00
2021-07-21 15:15:13 +00:00
renderTableRow = ( args : any ) = > {
2019-12-04 16:39:53 +00:00
const { theme } = this . props ;
return < MarkdownTableRow { ...args } theme = { theme } / > ;
}
2019-08-27 12:25:38 +00:00
2021-07-21 15:15:13 +00:00
renderTableCell = ( args : any ) = > {
2019-12-04 16:39:53 +00:00
const { theme } = this . props ;
return < MarkdownTableCell { ...args } theme = { theme } / > ;
}
2019-08-27 12:25:38 +00:00
render() {
2020-02-28 16:18:03 +00:00
const {
2020-03-06 18:13:33 +00:00
msg , numberOfLines , preview = false , theme , style = [ ] , testID
2020-02-28 16:18:03 +00:00
} = this . props ;
2019-08-27 12:25:38 +00:00
if ( ! msg ) {
return null ;
}
let m = formatText ( msg ) ;
// Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test'
// Return: 'Test'
2021-04-01 20:54:31 +00:00
m = m . replace ( /^\[([\s]*)\]\(([^)]*)\)\s/ , '' ) . trim ( ) ;
2019-10-02 12:41:51 +00:00
if ( preview ) {
2020-02-28 16:18:03 +00:00
m = shortnameToUnicode ( m ) ;
2020-12-18 14:14:25 +00:00
// Removes sequential empty spaces
m = m . replace ( /\s+/g , ' ' ) ;
2020-02-28 16:18:03 +00:00
m = removeMarkdown ( m ) ;
2020-07-17 17:45:39 +00:00
m = m . replace ( /\n+/g , ' ' ) ;
2020-02-28 16:18:03 +00:00
return (
2020-03-06 18:13:33 +00:00
< Text accessibilityLabel = { m } style = { [ styles . text , { color : themes [ theme ] . bodyText } , . . . style ] } numberOfLines = { numberOfLines } testID = { testID } >
2020-02-28 16:18:03 +00:00
{ m }
< / Text >
) ;
2019-10-02 12:41:51 +00:00
}
2019-08-27 12:25:38 +00:00
2020-02-28 16:18:03 +00:00
let ast = parser . parse ( m ) ;
ast = mergeTextNodes ( ast ) ;
2019-11-27 20:53:14 +00:00
this . isMessageContainsOnlyEmoji = isOnlyEmoji ( m ) && emojiCount ( m ) <= 3 ;
2019-08-27 12:25:38 +00:00
this . editedMessage ( ast ) ;
return this . renderer . render ( ast ) ;
}
}
2019-12-04 16:39:53 +00:00
export default Markdown ;