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' ;
import PropTypes from 'prop-types' ;
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' ;
import MarkdownHashtag from './Hashtag' ;
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
// Support <http://link|Text>
const formatText = text => text . replace (
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 ( '|' ) ;
2019-11-27 20:53:14 +00:00
const removeSpaces = str => str && str . replace ( /\s/g , '' ) ;
2019-08-27 12:25:38 +00:00
const removeAllEmoji = str => str . replace ( new RegExp ( emojiRanges , 'g' ) , '' ) ;
2019-11-27 20:53:14 +00:00
const isOnlyEmoji = ( str ) => {
str = removeSpaces ( str ) ;
return ! removeAllEmoji ( str ) . length ;
} ;
2019-08-27 12:25:38 +00:00
const removeOneEmoji = str => str . replace ( new RegExp ( emojiRanges ) , '' ) ;
const emojiCount = ( str ) => {
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 ( ) ;
2019-12-04 16:39:53 +00:00
class Markdown extends PureComponent {
2019-08-27 12:25:38 +00:00
static propTypes = {
msg : PropTypes . string ,
getCustomEmoji : PropTypes . func ,
baseUrl : PropTypes . string ,
username : PropTypes . string ,
tmid : PropTypes . string ,
isEdited : PropTypes . bool ,
numberOfLines : PropTypes . number ,
2019-10-02 12:41:51 +00:00
customEmojis : PropTypes . bool ,
2020-02-21 16:13:05 +00:00
useRealName : PropTypes . bool ,
2019-08-27 12:25:38 +00:00
channels : PropTypes . oneOfType ( [ PropTypes . array , PropTypes . object ] ) ,
mentions : PropTypes . oneOfType ( [ PropTypes . array , PropTypes . object ] ) ,
2019-10-02 12:41:51 +00:00
navToRoomInfo : PropTypes . func ,
preview : PropTypes . bool ,
2019-12-04 16:39:53 +00:00
theme : PropTypes . string ,
2020-03-06 18:13:33 +00:00
testID : PropTypes . string ,
2019-10-02 12:41:51 +00:00
style : PropTypes . array
2019-08-27 12:25:38 +00:00
} ;
constructor ( props ) {
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
} ) ;
editedMessage = ( ast ) => {
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 ) ;
}
}
} ;
renderText = ( { context , literal } ) => {
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 }
< / T e x t >
) ;
}
2019-10-02 12:41:51 +00:00
renderCodeInline = ( { literal } ) => {
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 }
< / T e x t >
) ;
2019-10-02 12:41:51 +00:00
} ;
2019-08-27 12:25:38 +00:00
2019-10-02 12:41:51 +00:00
renderCodeBlock = ( { literal } ) => {
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 }
< / T e x t >
) ;
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' } < / T e x t > ;
}
renderParagraph = ( { children } ) => {
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 }
< / T e x t >
2019-08-27 12:25:38 +00:00
) ;
} ;
2019-10-04 13:28:36 +00:00
renderLink = ( { children , href } ) => {
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
< MarkdownLink
link = { href }
theme = { theme }
>
2019-10-04 13:28:36 +00:00
{ children }
< / M a r k d o w n L i n k >
) ;
}
2019-08-27 12:25:38 +00:00
renderHashtag = ( { hashtag } ) => {
2019-10-04 13:28:36 +00:00
const {
2020-02-28 16:18:03 +00:00
channels , navToRoomInfo , style , theme
2019-10-04 13:28:36 +00:00
} = 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
/ >
) ;
}
renderAtMention = ( { mentionName } ) => {
2019-10-02 12:41:51 +00:00
const {
2020-02-28 16:18:03 +00:00
username , mentions , navToRoomInfo , useRealName , style , theme
2019-10-02 12:41:51 +00:00
} = 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
/ >
) ;
}
2020-05-08 17:16:22 +00:00
renderEmoji = ( { literal } ) => {
2019-10-02 12:41:51 +00:00
const {
2020-05-08 17:16:22 +00:00
getCustomEmoji , baseUrl , customEmojis , style , theme
2019-10-02 12:41:51 +00:00
} = 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
/ >
) ;
}
2020-03-20 16:26:50 +00:00
renderImage = ( { src } ) => {
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' ) } ) < / T e x t > ;
}
2019-08-27 12:25:38 +00:00
renderHeading = ( { children , level } ) => {
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 }
< / T e x t >
) ;
} ;
renderList = ( {
children , start , tight , type
2019-10-02 12:41:51 +00:00
} ) => {
const { numberOfLines } = this . props ;
return (
< MarkdownList
ordered = { type !== 'bullet' }
start = { start }
tight = { tight }
numberOfLines = { numberOfLines }
>
{ children }
< / M a r k d o w n L i s t >
) ;
} ;
2019-08-27 12:25:38 +00:00
renderListItem = ( {
children , context , ... otherProps
} ) => {
2019-12-04 16:39:53 +00:00
const { theme } = this . props ;
2019-08-27 12:25:38 +00:00
const level = context . filter ( type => type === 'list' ) . length ;
return (
< MarkdownListItem
level = { level }
2019-12-04 16:39:53 +00:00
theme = { theme }
2019-08-27 12:25:38 +00:00
{ ... otherProps }
>
{ children }
< / M a r k d o w n L i s t I t e m >
) ;
} ;
2019-10-04 13:28:36 +00:00
renderBlockQuote = ( { children } ) => {
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 }
< / M a r k d o w n B l o c k Q u o t e >
) ;
}
2019-08-27 12:25:38 +00:00
2019-12-04 16:39:53 +00:00
renderTable = ( { children , numColumns } ) => {
const { theme } = this . props ;
return (
< MarkdownTable numColumns = { numColumns } theme = { theme } >
{ children }
< / M a r k d o w n T a b l e >
) ;
}
2019-08-27 12:25:38 +00:00
2019-12-04 16:39:53 +00:00
renderTableRow = ( args ) => {
const { theme } = this . props ;
return < MarkdownTableRow { ... args } theme = { theme } / > ;
}
2019-08-27 12:25:38 +00:00
2019-12-04 16:39:53 +00:00
renderTableCell = ( args ) => {
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 }
< / T e x t >
) ;
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 ;