diff --git a/src/RichText.js b/src/RichText.js index f1f2188d0d..c0d80d2ec7 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -21,6 +21,15 @@ const STYLES = { UNDERLINE: 'u' }; +const MARKDOWN_REGEX = { + LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g, + ITALIC: /([\*_])([\w\s]+?)\1/g, + BOLD: /([\*_])\1([\w\s]+?)\1\1/g +}; + +const USERNAME_REGEX = /@\S+:\S+/g; +const ROOM_REGEX = /#\S+:\S+/g; + export function contentStateToHTML(contentState: ContentState): string { return contentState.getBlockMap().map((block) => { let elem = BLOCK_RENDER_MAP.get(block.getType()).element; @@ -46,13 +55,10 @@ export function HTMLtoContentState(html: string): ContentState { return ContentState.createFromBlockArray(convertFromHTML(html)); } -const USERNAME_REGEX = /@\S+:\S+/g; -const ROOM_REGEX = /#\S+:\S+/g; - /** * Returns a composite decorator which has access to provided scope. */ -export function getScopedDecorator(scope: any): CompositeDecorator { +export function getScopedRTDecorators(scope: any): CompositeDecorator { let MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); let usernameDecorator = { @@ -78,7 +84,34 @@ export function getScopedDecorator(scope: any): CompositeDecorator { } }; - return new CompositeDecorator([usernameDecorator, roomDecorator]); + return [usernameDecorator, roomDecorator]; +} + +export function getScopedMDDecorators(scope: any): CompositeDecorator { + let markdownDecorators = ['BOLD', 'ITALIC'].map( + (style) => ({ + strategy: (contentBlock, callback) => { + return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback); + }, + component: (props) => ( + + {props.children} + + ) + })); + + markdownDecorators.push({ + strategy: (contentBlock, callback) => { + return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback); + }, + component: (props) => ( + + {props.children} + + ) + }); + + return markdownDecorators; } /** @@ -97,15 +130,27 @@ function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: numb /** * Passes rangeToReplace to modifyFn and replaces it in contentState with the result. */ -export function modifyText(contentState: ContentState, rangeToReplace: SelectionState, modifyFn: (text: string) => string, ...rest): ContentState { - let startKey = rangeToReplace.getStartKey(), - endKey = contentState.getKeyAfter(rangeToReplace.getEndKey()), +export function modifyText(contentState: ContentState, rangeToReplace: SelectionState, + modifyFn: (text: string) => string, ...rest): ContentState { + let getText = (key) => contentState.getBlockForKey(key).getText(), + startKey = rangeToReplace.getStartKey(), + startOffset = rangeToReplace.getStartOffset(), + endKey = rangeToReplace.getEndKey(), + endOffset = rangeToReplace.getEndOffset(), text = ""; - for(let currentKey = startKey; currentKey && currentKey !== endKey; currentKey = contentState.getKeyAfter(currentKey)) { - let currentBlock = contentState.getBlockForKey(currentKey); - text += currentBlock.getText(); + + for(let currentKey = startKey; + currentKey && currentKey !== endKey; + currentKey = contentState.getKeyAfter(currentKey)) { + text += getText(currentKey).substring(startOffset, blockText.length); + + // from now on, we'll take whole blocks + startOffset = 0; } + // add remaining part of last block + text += getText(endKey).substring(startOffset, endOffset); + return Modifier.replaceText(contentState, rangeToReplace, modifyFn(text), ...rest); } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 3c5ddf1512..da61631b4d 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -97,13 +97,16 @@ export default class MessageComposerInput extends React.Component { * - whether we've got rich text mode enabled * - contentState was passed in */ - createEditorState(contentState: ?ContentState): EditorState { - let func = contentState ? EditorState.createWithContent : EditorState.createEmpty; - let args = contentState ? [contentState] : []; - if(this.state.isRichtextEnabled) { - args.push(RichText.getScopedDecorator(this.props)); + createEditorState(richText: boolean, contentState: ?ContentState): EditorState { + let decorators = richText ? RichText.getScopedRTDecorators(this.props) : + RichText.getScopedMDDecorators(this.props), + compositeDecorator = new CompositeDecorator(decorators); + + if (contentState) { + return EditorState.createWithContent(contentState, compositeDecorator); + } else { + return EditorState.createEmpty(compositeDecorator); } - return func(...args); } componentWillMount() { @@ -194,7 +197,7 @@ export default class MessageComposerInput extends React.Component { if (contentJSON) { let content = convertFromRaw(JSON.parse(contentJSON)); component.setState({ - editorState: component.createEditorState(content) + editorState: component.createEditorState(this.state.isRichtextEnabled, content) }); } } @@ -341,22 +344,22 @@ export default class MessageComposerInput extends React.Component { } enableRichtext(enabled: boolean) { + if (enabled) { + let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText()); + this.setState({ + editorState: this.createEditorState(enabled, RichText.HTMLtoContentState(html)) + }); + } else { + let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()), + contentState = ContentState.createFromText(markdown); + this.setState({ + editorState: this.createEditorState(enabled, contentState) + }); + } + this.setState({ isRichtextEnabled: enabled }); - - if(!this.state.isRichtextEnabled) { - let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText()); - this.setState({ - editorState: this.createEditorState(RichText.HTMLtoContentState(html)) - }); - } else { - let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()); - let contentState = ContentState.createFromText(markdown); - this.setState({ - editorState: this.createEditorState(contentState) - }); - } } handleKeyCommand(command: string): boolean {