2018-05-06 21:08:36 +00:00
/ *
Copyright 2015 - 2017 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
2016-07-03 16:45:13 +00:00
import React from 'react' ;
2018-05-06 21:08:36 +00:00
/ *
2016-06-11 16:54:09 +00:00
import {
Editor ,
2016-07-08 07:24:28 +00:00
EditorState ,
2016-06-11 16:54:09 +00:00
Modifier ,
ContentState ,
2016-07-03 16:45:13 +00:00
ContentBlock ,
2016-06-11 16:54:09 +00:00
convertFromHTML ,
DefaultDraftBlockRenderMap ,
DefaultDraftInlineStyle ,
2016-06-21 10:16:20 +00:00
CompositeDecorator ,
2016-07-03 16:45:13 +00:00
SelectionState ,
2016-07-08 07:24:28 +00:00
Entity ,
2016-06-11 16:54:09 +00:00
} from 'draft-js' ;
2018-05-06 21:08:36 +00:00
import { stateToMarkdown as _ _stateToMarkdown } from 'draft-js-export-markdown' ;
* /
import Html from 'slate-html-serializer' ;
2017-01-20 14:22:27 +00:00
import * as sdk from './index' ;
2016-07-02 19:41:34 +00:00
import * as emojione from 'emojione' ;
2018-05-06 21:08:36 +00:00
import { SelectionRange } from "./autocomplete/Autocompleter" ;
2016-05-27 04:45:55 +00:00
2016-06-11 22:52:30 +00:00
const MARKDOWN _REGEX = {
LINK : /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g ,
ITALIC : /([\*_])([\w\s]+?)\1/g ,
2016-07-04 16:14:35 +00:00
BOLD : /([\*_])\1([\w\s]+?)\1\1/g ,
2016-09-07 17:22:14 +00:00
HR : /(\n|^)((-|\*|_) *){3,}(\n|$)/g ,
CODE : /`[^`]*`/g ,
2016-09-07 21:16:56 +00:00
STRIKETHROUGH : /~{2}[^~]*~{2}/g ,
2016-06-11 22:52:30 +00:00
} ;
const USERNAME _REGEX = /@\S+:\S+/g ;
const ROOM _REGEX = /#\S+:\S+/g ;
2016-07-04 16:14:35 +00:00
const EMOJI _REGEX = new RegExp ( emojione . unicodeRegexp , 'g' ) ;
2016-06-11 22:52:30 +00:00
2017-03-10 15:04:31 +00:00
const ZWS _CODE = 8203 ;
const ZWS = String . fromCharCode ( ZWS _CODE ) ; // zero width space
2018-05-06 21:08:36 +00:00
2017-03-10 15:04:31 +00:00
export function stateToMarkdown ( state ) {
return _ _stateToMarkdown ( state )
. replace (
ZWS , // draft-js-export-markdown adds these
'' ) ; // this is *not* a zero width space, trust me :)
}
2018-05-06 21:08:36 +00:00
export const editorStateToHTML = ( editorState : Value ) => {
return Html . deserialize ( editorState ) ;
}
2016-05-27 04:45:55 +00:00
2018-05-06 21:08:36 +00:00
export function htmlToEditorState ( html : string ) : Value {
return Html . serialize ( html ) ;
2016-05-27 04:45:55 +00:00
}
2016-06-09 18:23:09 +00:00
2016-07-08 07:24:28 +00:00
function unicodeToEmojiUri ( str ) {
let replaceWith , unicode , alt ;
if ( ( ! emojione . unicodeAlt ) || ( emojione . sprites ) ) {
// if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
2017-10-11 16:56:17 +00:00
const mappedUnicode = emojione . mapUnicodeToShort ( ) ;
2016-07-08 07:24:28 +00:00
}
str = str . replace ( emojione . regUnicode , function ( unicodeChar ) {
if ( ( typeof unicodeChar === 'undefined' ) || ( unicodeChar === '' ) || ( ! ( unicodeChar in emojione . jsEscapeMap ) ) ) {
// if the unicodeChar doesnt exist just return the entire match
return unicodeChar ;
} else {
2017-10-11 06:39:46 +00:00
// Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below
2017-11-16 13:19:36 +00:00
if ( unicodeChar . length == 2 && unicodeChar [ 1 ] == '\ufe0f' ) {
2017-10-11 06:39:46 +00:00
unicodeChar = unicodeChar [ 0 ] ;
}
2016-07-08 07:24:28 +00:00
// get the unicode codepoint from the actual char
unicode = emojione . jsEscapeMap [ unicodeChar ] ;
2017-10-11 06:39:46 +00:00
2016-07-08 07:24:28 +00:00
return emojione . imagePathSVG + unicode + '.svg' + emojione . cacheBustParam ;
}
} ) ;
return str ;
}
2016-09-04 15:33:40 +00:00
/ * *
* Utility function that looks for regex matches within a ContentBlock and invokes { callback } with ( start , end )
* From https : //facebook.github.io/draft-js/docs/advanced-topics-decorators.html
* /
function findWithRegex ( regex , contentBlock : ContentBlock , callback : ( start : number , end : number ) => any ) {
const text = contentBlock . getText ( ) ;
let matchArr , start ;
while ( ( matchArr = regex . exec ( text ) ) !== null ) {
start = matchArr . index ;
callback ( start , start + matchArr [ 0 ] . length ) ;
}
}
2016-08-03 12:57:49 +00:00
// Workaround for https://github.com/facebook/draft-js/issues/414
2017-10-11 16:56:17 +00:00
const emojiDecorator = {
2017-08-03 11:02:29 +00:00
strategy : ( contentState , contentBlock , callback ) => {
2016-07-08 07:24:28 +00:00
findWithRegex ( EMOJI _REGEX , contentBlock , callback ) ;
} ,
component : ( props ) => {
2017-10-11 16:56:17 +00:00
const uri = unicodeToEmojiUri ( props . children [ 0 ] . props . text ) ;
const shortname = emojione . toShort ( props . children [ 0 ] . props . text ) ;
const style = {
2016-07-08 07:24:28 +00:00
display : 'inline-block' ,
width : '1em' ,
maxHeight : '1em' ,
background : ` url( ${ uri } ) ` ,
backgroundSize : 'contain' ,
backgroundPosition : 'center center' ,
overflow : 'hidden' ,
} ;
2017-10-11 16:56:17 +00:00
return ( < span title = { shortname } style = { style } > < span style = { { opacity : 0 } } > { props . children } < / s p a n > < / s p a n > ) ;
2016-07-08 07:24:28 +00:00
} ,
} ;
2016-06-11 10:22:08 +00:00
/ * *
* Returns a composite decorator which has access to provided scope .
* /
2016-06-11 21:13:57 +00:00
export function getScopedRTDecorators ( scope : any ) : CompositeDecorator {
2016-09-15 21:17:27 +00:00
return [ emojiDecorator ] ;
2016-06-09 18:23:09 +00:00
}
2016-06-11 21:13:57 +00:00
export function getScopedMDDecorators ( scope : any ) : CompositeDecorator {
2017-10-11 16:56:17 +00:00
const markdownDecorators = [ 'HR' , 'BOLD' , 'ITALIC' , 'CODE' , 'STRIKETHROUGH' ] . map (
2016-06-11 21:13:57 +00:00
( style ) => ( {
2017-08-03 11:02:29 +00:00
strategy : ( contentState , contentBlock , callback ) => {
2016-06-11 21:13:57 +00:00
return findWithRegex ( MARKDOWN _REGEX [ style ] , contentBlock , callback ) ;
} ,
component : ( props ) => (
< span className = { "mx_MarkdownElement mx_Markdown_" + style } >
2017-10-11 16:56:17 +00:00
{ props . children }
2016-06-11 21:13:57 +00:00
< / s p a n >
2017-10-11 16:56:17 +00:00
) ,
2016-06-11 21:13:57 +00:00
} ) ) ;
markdownDecorators . push ( {
2017-08-03 11:02:29 +00:00
strategy : ( contentState , contentBlock , callback ) => {
2016-06-11 21:13:57 +00:00
return findWithRegex ( MARKDOWN _REGEX . LINK , contentBlock , callback ) ;
} ,
component : ( props ) => (
< a href = "#" className = "mx_MarkdownElement mx_Markdown_LINK" >
2017-10-11 16:56:17 +00:00
{ props . children }
2016-06-11 21:13:57 +00:00
< / a >
2017-10-11 16:56:17 +00:00
) ,
2016-06-11 21:13:57 +00:00
} ) ;
2016-10-11 13:46:35 +00:00
// markdownDecorators.push(emojiDecorator);
// TODO Consider renabling "syntax highlighting" when we can do it properly
return [ emojiDecorator ] ;
2016-06-11 21:13:57 +00:00
}
2016-06-11 16:54:09 +00:00
/ * *
* Passes rangeToReplace to modifyFn and replaces it in contentState with the result .
* /
2016-06-11 22:50:30 +00:00
export function modifyText ( contentState : ContentState , rangeToReplace : SelectionState ,
2016-06-14 13:40:35 +00:00
modifyFn : ( text : string ) => string , inlineStyle , entityKey ) : ContentState {
2016-06-11 22:50:30 +00:00
let getText = ( key ) => contentState . getBlockForKey ( key ) . getText ( ) ,
startKey = rangeToReplace . getStartKey ( ) ,
startOffset = rangeToReplace . getStartOffset ( ) ,
endKey = rangeToReplace . getEndKey ( ) ,
endOffset = rangeToReplace . getEndOffset ( ) ,
2016-06-11 16:54:09 +00:00
text = "" ;
2016-06-11 22:50:30 +00:00
2016-07-03 16:45:13 +00:00
for ( let currentKey = startKey ;
2016-06-11 22:50:30 +00:00
currentKey && currentKey !== endKey ;
currentKey = contentState . getKeyAfter ( currentKey ) ) {
2017-10-11 16:56:17 +00:00
const blockText = getText ( currentKey ) ;
2016-06-14 13:58:51 +00:00
text += blockText . substring ( startOffset , blockText . length ) ;
2016-06-11 22:50:30 +00:00
// from now on, we'll take whole blocks
startOffset = 0 ;
2016-06-11 16:54:09 +00:00
}
2016-06-11 22:50:30 +00:00
// add remaining part of last block
text += getText ( endKey ) . substring ( startOffset , endOffset ) ;
2016-06-14 13:40:35 +00:00
return Modifier . replaceText ( contentState , rangeToReplace , modifyFn ( text ) , inlineStyle , entityKey ) ;
2016-06-11 16:54:09 +00:00
}
2016-06-21 10:16:20 +00:00
/ * *
* Computes the plaintext offsets of the given SelectionState .
* Note that this inherently means we make assumptions about what that means ( no separator between ContentBlocks , etc )
* Used by autocomplete to show completions when the current selection lies within , or at the edges of a command .
* /
2016-07-03 16:45:13 +00:00
export function selectionStateToTextOffsets ( selectionState : SelectionState ,
contentBlocks : Array < ContentBlock > ) : { start : number , end : number } {
2016-06-21 10:16:20 +00:00
let offset = 0 , start = 0 , end = 0 ;
2017-10-11 16:56:17 +00:00
for ( const block of contentBlocks ) {
2016-07-03 16:45:13 +00:00
if ( selectionState . getStartKey ( ) === block . getKey ( ) ) {
2016-06-21 10:16:20 +00:00
start = offset + selectionState . getStartOffset ( ) ;
}
2016-07-03 16:45:13 +00:00
if ( selectionState . getEndKey ( ) === block . getKey ( ) ) {
2016-06-21 10:16:20 +00:00
end = offset + selectionState . getEndOffset ( ) ;
break ;
}
offset += block . getLength ( ) ;
}
return {
start ,
2016-07-03 16:45:13 +00:00
end ,
} ;
}
2016-07-08 07:24:28 +00:00
// modified version of https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-emoji-plugin/src/modifiers/attachImmutableEntitiesToEmojis.js
export function attachImmutableEntitiesToEmoji ( editorState : EditorState ) : EditorState {
const contentState = editorState . getCurrentContent ( ) ;
const blocks = contentState . getBlockMap ( ) ;
let newContentState = contentState ;
blocks . forEach ( ( block ) => {
const plainText = block . getText ( ) ;
const addEntityToEmoji = ( start , end ) => {
const existingEntityKey = block . getEntityAt ( start ) ;
if ( existingEntityKey ) {
// avoid manipulation in case the emoji already has an entity
2017-08-03 10:29:26 +00:00
const entity = newContentState . getEntity ( existingEntityKey ) ;
2016-07-08 07:24:28 +00:00
if ( entity && entity . get ( 'type' ) === 'emoji' ) {
return ;
}
}
const selection = SelectionState . createEmpty ( block . getKey ( ) )
. set ( 'anchorOffset' , start )
. set ( 'focusOffset' , end ) ;
const emojiText = plainText . substring ( start , end ) ;
2017-08-03 10:18:56 +00:00
newContentState = newContentState . createEntity (
2017-10-11 16:56:17 +00:00
'emoji' , 'IMMUTABLE' , { emojiUnicode : emojiText } ,
2017-08-03 10:18:56 +00:00
) ;
const entityKey = newContentState . getLastCreatedEntityKey ( ) ;
2016-07-08 07:24:28 +00:00
newContentState = Modifier . replaceText (
newContentState ,
selection ,
emojiText ,
null ,
entityKey ,
) ;
} ;
findWithRegex ( EMOJI _REGEX , block , addEntityToEmoji ) ;
} ) ;
if ( ! newContentState . equals ( contentState ) ) {
2016-09-21 01:32:53 +00:00
const oldSelection = editorState . getSelection ( ) ;
editorState = EditorState . push (
2016-07-08 07:24:28 +00:00
editorState ,
newContentState ,
'convert-to-immutable-emojis' ,
) ;
2016-09-21 01:32:53 +00:00
// this is somewhat of a hack, we're undoing selection changes caused above
// it would be better not to make those changes in the first place
editorState = EditorState . forceSelection ( editorState , oldSelection ) ;
2016-07-08 07:24:28 +00:00
}
return editorState ;
}
2017-07-19 14:00:25 +00:00
export function hasMultiLineSelection ( editorState : EditorState ) : boolean {
const selectionState = editorState . getSelection ( ) ;
const anchorKey = selectionState . getAnchorKey ( ) ;
const currentContent = editorState . getCurrentContent ( ) ;
const currentContentBlock = currentContent . getBlockForKey ( anchorKey ) ;
const start = selectionState . getStartOffset ( ) ;
const end = selectionState . getEndOffset ( ) ;
const selectedText = currentContentBlock . getText ( ) . slice ( start , end ) ;
return selectedText . includes ( '\n' ) ;
}