/*global require,module*/
'use strict' ;
var CodeMirror = require ( 'codemirror' ) ;
require ( 'codemirror/addon/edit/continuelist.js' ) ;
require ( './codemirror/tablist' ) ;
require ( 'codemirror/addon/display/fullscreen.js' ) ;
require ( 'codemirror/mode/markdown/markdown.js' ) ;
require ( 'codemirror/addon/mode/overlay.js' ) ;
require ( 'codemirror/addon/display/placeholder.js' ) ;
require ( 'codemirror/addon/selection/mark-selection.js' ) ;
require ( 'codemirror/mode/gfm/gfm.js' ) ;
require ( 'codemirror/mode/xml/xml.js' ) ;
var CodeMirrorSpellChecker = require ( 'codemirror-spell-checker' ) ;
var marked = require ( 'marked' ) ;
// Some variables
var isMac = /Mac/ . test ( navigator . platform ) ;
var anchorToExternalRegex = new RegExp ( /(<a.*?https?:\/\/.*?[^a]>)+?/g ) ;
// Mapping of actions that can be bound to keyboard shortcuts or toolbar buttons
var bindings = {
'toggleBold' : toggleBold ,
'toggleItalic' : toggleItalic ,
'drawLink' : drawLink ,
'toggleHeadingSmaller' : toggleHeadingSmaller ,
'toggleHeadingBigger' : toggleHeadingBigger ,
'drawImage' : drawImage ,
'toggleBlockquote' : toggleBlockquote ,
'toggleOrderedList' : toggleOrderedList ,
'toggleUnorderedList' : toggleUnorderedList ,
'toggleCodeBlock' : toggleCodeBlock ,
'togglePreview' : togglePreview ,
'toggleStrikethrough' : toggleStrikethrough ,
'toggleHeading1' : toggleHeading1 ,
'toggleHeading2' : toggleHeading2 ,
'toggleHeading3' : toggleHeading3 ,
'cleanBlock' : cleanBlock ,
'drawTable' : drawTable ,
'drawHorizontalRule' : drawHorizontalRule ,
'undo' : undo ,
'redo' : redo ,
'toggleSideBySide' : toggleSideBySide ,
'toggleFullScreen' : toggleFullScreen
} ;
var shortcuts = {
'toggleBold' : 'Cmd-B' ,
'toggleItalic' : 'Cmd-I' ,
'drawLink' : 'Cmd-K' ,
'toggleHeadingSmaller' : 'Cmd-H' ,
'toggleHeadingBigger' : 'Shift-Cmd-H' ,
'cleanBlock' : 'Cmd-E' ,
'drawImage' : 'Cmd-Alt-I' ,
'toggleBlockquote' : 'Cmd-\'' ,
'toggleOrderedList' : 'Cmd-Alt-L' ,
'toggleUnorderedList' : 'Cmd-L' ,
'toggleCodeBlock' : 'Cmd-Alt-C' ,
'togglePreview' : 'Cmd-P' ,
'toggleSideBySide' : 'F9' ,
'toggleFullScreen' : 'F11'
} ;
var getBindingName = function ( f ) {
for ( var key in bindings ) {
if ( bindings [ key ] === f ) {
return key ;
}
}
return null ;
} ;
var isMobile = function ( ) {
var check = false ;
( function ( a ) {
if ( /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i . test ( a ) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i . test ( a . substr ( 0 , 4 ) ) ) check = true ;
} ) ( navigator . userAgent || navigator . vendor || window . opera ) ;
return check ;
} ;
/ * *
* Modify HTML to add 'target="_blank"' to links so they open in new tabs by default .
* @ param { string } htmlText - HTML to be modified .
* @ return { string } The modified HTML text .
* /
function addAnchorTargetBlank ( htmlText ) {
var match ;
while ( ( match = anchorToExternalRegex . exec ( htmlText ) ) !== null ) {
// With only one capture group in the RegExp, we can safely take the first index from the match.
var linkString = match [ 0 ] ;
if ( linkString . indexOf ( 'target=' ) === - 1 ) {
var fixedLinkString = linkString . replace ( />$/ , ' target="_blank">' ) ;
htmlText = htmlText . replace ( linkString , fixedLinkString ) ;
}
}
return htmlText ;
}
/ * *
* Fix shortcut . Mac use Command , others use Ctrl .
* /
function fixShortcut ( name ) {
if ( isMac ) {
name = name . replace ( 'Ctrl' , 'Cmd' ) ;
} else {
name = name . replace ( 'Cmd' , 'Ctrl' ) ;
}
return name ;
}
/ * *
* Create icon element for toolbar .
* /
function createIcon ( options , enableTooltips , shortcuts ) {
options = options || { } ;
var el = document . createElement ( 'button' ) ;
enableTooltips = ( enableTooltips == undefined ) ? true : enableTooltips ;
if ( options . title && enableTooltips ) {
el . title = createTooltip ( options . title , options . action , shortcuts ) ;
if ( isMac ) {
el . title = el . title . replace ( 'Ctrl' , '⌘' ) ;
el . title = el . title . replace ( 'Alt' , '⌥' ) ;
}
}
if ( options . noDisable ) {
el . classList . add ( 'no-disable' ) ;
}
if ( options . noMobile ) {
el . classList . add ( 'no-mobile' ) ;
}
el . tabIndex = - 1 ;
// Create icon element and append as a child to the button
var icon = document . createElement ( 'i' ) ;
icon . className = options . className ;
el . appendChild ( icon ) ;
return el ;
}
function createSep ( ) {
var el = document . createElement ( 'i' ) ;
el . className = 'separator' ;
el . innerHTML = '|' ;
return el ;
}
function createTooltip ( title , action , shortcuts ) {
var actionName ;
var tooltip = title ;
if ( action ) {
actionName = getBindingName ( action ) ;
if ( shortcuts [ actionName ] ) {
tooltip += ' (' + fixShortcut ( shortcuts [ actionName ] ) + ')' ;
}
}
return tooltip ;
}
/ * *
* The state of CodeMirror at the given position .
* /
function getState ( cm , pos ) {
pos = pos || cm . getCursor ( 'start' ) ;
var stat = cm . getTokenAt ( pos ) ;
if ( ! stat . type ) return { } ;
var types = stat . type . split ( ' ' ) ;
var ret = { } ,
data , text ;
for ( var i = 0 ; i < types . length ; i ++ ) {
data = types [ i ] ;
if ( data === 'strong' ) {
ret . bold = true ;
} else if ( data === 'variable-2' ) {
text = cm . getLine ( pos . line ) ;
if ( /^\s*\d+\.\s/ . test ( text ) ) {
ret [ 'ordered-list' ] = true ;
} else {
ret [ 'unordered-list' ] = true ;
}
} else if ( data === 'atom' ) {
ret . quote = true ;
} else if ( data === 'em' ) {
ret . italic = true ;
} else if ( data === 'quote' ) {
ret . quote = true ;
} else if ( data === 'strikethrough' ) {
ret . strikethrough = true ;
} else if ( data === 'comment' ) {
ret . code = true ;
} else if ( data === 'link' ) {
ret . link = true ;
} else if ( data === 'tag' ) {
ret . image = true ;
} else if ( data . match ( /^header(-[1-6])?$/ ) ) {
ret [ data . replace ( 'header' , 'heading' ) ] = true ;
}
}
return ret ;
}
// Saved overflow setting
var saved _overflow = '' ;
/ * *
* Toggle full screen of the editor .
* /
function toggleFullScreen ( editor ) {
// Set fullscreen
var cm = editor . codemirror ;
cm . setOption ( 'fullScreen' , ! cm . getOption ( 'fullScreen' ) ) ;
// Prevent scrolling on body during fullscreen active
if ( cm . getOption ( 'fullScreen' ) ) {
saved _overflow = document . body . style . overflow ;
document . body . style . overflow = 'hidden' ;
} else {
document . body . style . overflow = saved _overflow ;
}
// Update toolbar class
var wrap = cm . getWrapperElement ( ) ;
if ( ! /fullscreen/ . test ( wrap . previousSibling . className ) ) {
wrap . previousSibling . className += ' fullscreen' ;
} else {
wrap . previousSibling . className = wrap . previousSibling . className . replace ( /\s*fullscreen\b/ , '' ) ;
}
// Update toolbar button
if ( editor . toolbarElements . fullscreen ) {
var toolbarButton = editor . toolbarElements . fullscreen ;
if ( ! /active/ . test ( toolbarButton . className ) ) {
toolbarButton . className += ' active' ;
} else {
toolbarButton . className = toolbarButton . className . replace ( /\s*active\s*/g , '' ) ;
}
}
// Hide side by side if needed
var sidebyside = cm . getWrapperElement ( ) . nextSibling ;
if ( /editor-preview-active-side/ . test ( sidebyside . className ) )
toggleSideBySide ( editor ) ;
}
/ * *
* Action for toggling bold .
* /
function toggleBold ( editor ) {
_toggleBlock ( editor , 'bold' , editor . options . blockStyles . bold ) ;
}
/ * *
* Action for toggling italic .
* /
function toggleItalic ( editor ) {
_toggleBlock ( editor , 'italic' , editor . options . blockStyles . italic ) ;
}
/ * *
* Action for toggling strikethrough .
* /
function toggleStrikethrough ( editor ) {
_toggleBlock ( editor , 'strikethrough' , '~~' ) ;
}
/ * *
* Action for toggling code block .
* /
function toggleCodeBlock ( editor ) {
var fenceCharsToInsert = editor . options . blockStyles . code ;
function fencing _line ( line ) {
/* return true, if this is a ``` or ~~~ line */
if ( typeof line !== 'object' ) {
throw 'fencing_line() takes a \'line\' object (not a line number, or line text). Got: ' + typeof line + ': ' + line ;
}
return line . styles && line . styles [ 2 ] && line . styles [ 2 ] . indexOf ( 'formatting-code-block' ) !== - 1 ;
}
function token _state ( token ) {
// base goes an extra level deep when mode backdrops are used, e.g. spellchecker on
return token . state . base . base || token . state . base ;
}
function code _type ( cm , line _num , line , firstTok , lastTok ) {
/ *
* Return "single" , "indented" , "fenced" or false
*
* cm and line _num are required . Others are optional for efficiency
* To check in the middle of a line , pass in firstTok yourself .
* /
line = line || cm . getLineHandle ( line _num ) ;
firstTok = firstTok || cm . getTokenAt ( {
line : line _num ,
ch : 1
} ) ;
lastTok = lastTok || ( ! ! line . text && cm . getTokenAt ( {
line : line _num ,
ch : line . text . length - 1
} ) ) ;
var types = firstTok . type ? firstTok . type . split ( ' ' ) : [ ] ;
if ( lastTok && token _state ( lastTok ) . indentedCode ) {
// have to check last char, since first chars of first line aren"t marked as indented
return 'indented' ;
} else if ( types . indexOf ( 'comment' ) === - 1 ) {
// has to be after "indented" check, since first chars of first indented line aren"t marked as such
return false ;
} else if ( token _state ( firstTok ) . fencedChars || token _state ( lastTok ) . fencedChars || fencing _line ( line ) ) {
return 'fenced' ;
} else {
return 'single' ;
}
}
function insertFencingAtSelection ( cm , cur _start , cur _end , fenceCharsToInsert ) {
var start _line _sel = cur _start . line + 1 ,
end _line _sel = cur _end . line + 1 ,
sel _multi = cur _start . line !== cur _end . line ,
repl _start = fenceCharsToInsert + '\n' ,
repl _end = '\n' + fenceCharsToInsert ;
if ( sel _multi ) {
end _line _sel ++ ;
}
// handle last char including \n or not
if ( sel _multi && cur _end . ch === 0 ) {
repl _end = fenceCharsToInsert + '\n' ;
end _line _sel -- ;
}
_replaceSelection ( cm , false , [ repl _start , repl _end ] ) ;
cm . setSelection ( {
line : start _line _sel ,
ch : 0
} , {
line : end _line _sel ,
ch : 0
} ) ;
}
var cm = editor . codemirror ,
cur _start = cm . getCursor ( 'start' ) ,
cur _end = cm . getCursor ( 'end' ) ,
tok = cm . getTokenAt ( {
line : cur _start . line ,
ch : cur _start . ch || 1
} ) , // avoid ch 0 which is a cursor pos but not token
line = cm . getLineHandle ( cur _start . line ) ,
is _code = code _type ( cm , cur _start . line , line , tok ) ;
var block _start , block _end , lineCount ;
if ( is _code === 'single' ) {
// similar to some EasyMDE _toggleBlock logic
var start = line . text . slice ( 0 , cur _start . ch ) . replace ( '`' , '' ) ,
end = line . text . slice ( cur _start . ch ) . replace ( '`' , '' ) ;
cm . replaceRange ( start + end , {
line : cur _start . line ,
ch : 0
} , {
line : cur _start . line ,
ch : 99999999999999
} ) ;
cur _start . ch -- ;
if ( cur _start !== cur _end ) {
cur _end . ch -- ;
}
cm . setSelection ( cur _start , cur _end ) ;
cm . focus ( ) ;
} else if ( is _code === 'fenced' ) {
if ( cur _start . line !== cur _end . line || cur _start . ch !== cur _end . ch ) {
// use selection
// find the fenced line so we know what type it is (tilde, backticks, number of them)
for ( block _start = cur _start . line ; block _start >= 0 ; block _start -- ) {
line = cm . getLineHandle ( block _start ) ;
if ( fencing _line ( line ) ) {
break ;
}
}
var fencedTok = cm . getTokenAt ( {
line : block _start ,
ch : 1
} ) ;
var fence _chars = token _state ( fencedTok ) . fencedChars ;
var start _text , start _line ;
var end _text , end _line ;
// check for selection going up against fenced lines, in which case we don't want to add more fencing
if ( fencing _line ( cm . getLineHandle ( cur _start . line ) ) ) {
start _text = '' ;
start _line = cur _start . line ;
} else if ( fencing _line ( cm . getLineHandle ( cur _start . line - 1 ) ) ) {
start _text = '' ;
start _line = cur _start . line - 1 ;
} else {
start _text = fence _chars + '\n' ;
start _line = cur _start . line ;
}
if ( fencing _line ( cm . getLineHandle ( cur _end . line ) ) ) {
end _text = '' ;
end _line = cur _end . line ;
if ( cur _end . ch === 0 ) {
end _line += 1 ;
}
} else if ( cur _end . ch !== 0 && fencing _line ( cm . getLineHandle ( cur _end . line + 1 ) ) ) {
end _text = '' ;
end _line = cur _end . line + 1 ;
} else {
end _text = fence _chars + '\n' ;
end _line = cur _end . line + 1 ;
}
if ( cur _end . ch === 0 ) {
// full last line selected, putting cursor at beginning of next
end _line -= 1 ;
}
cm . operation ( function ( ) {
// end line first, so that line numbers don't change
cm . replaceRange ( end _text , {
line : end _line ,
ch : 0
} , {
line : end _line + ( end _text ? 0 : 1 ) ,
ch : 0
} ) ;
cm . replaceRange ( start _text , {
line : start _line ,
ch : 0
} , {
line : start _line + ( start _text ? 0 : 1 ) ,
ch : 0
} ) ;
} ) ;
cm . setSelection ( {
line : start _line + ( start _text ? 1 : 0 ) ,
ch : 0
} , {
line : end _line + ( start _text ? 1 : - 1 ) ,
ch : 0
} ) ;
cm . focus ( ) ;
} else {
// no selection, search for ends of this fenced block
var search _from = cur _start . line ;
if ( fencing _line ( cm . getLineHandle ( cur _start . line ) ) ) { // gets a little tricky if cursor is right on a fenced line
if ( code _type ( cm , cur _start . line + 1 ) === 'fenced' ) {
block _start = cur _start . line ;
search _from = cur _start . line + 1 ; // for searching for "end"
} else {
block _end = cur _start . line ;
search _from = cur _start . line - 1 ; // for searching for "start"
}
}
if ( block _start === undefined ) {
for ( block _start = search _from ; block _start >= 0 ; block _start -- ) {
line = cm . getLineHandle ( block _start ) ;
if ( fencing _line ( line ) ) {
break ;
}
}
}
if ( block _end === undefined ) {
lineCount = cm . lineCount ( ) ;
for ( block _end = search _from ; block _end < lineCount ; block _end ++ ) {
line = cm . getLineHandle ( block _end ) ;
if ( fencing _line ( line ) ) {
break ;
}
}
}
cm . operation ( function ( ) {
cm . replaceRange ( '' , {
line : block _start ,
ch : 0
} , {
line : block _start + 1 ,
ch : 0
} ) ;
cm . replaceRange ( '' , {
line : block _end - 1 ,
ch : 0
} , {
line : block _end ,
ch : 0
} ) ;
} ) ;
cm . focus ( ) ;
}
} else if ( is _code === 'indented' ) {
if ( cur _start . line !== cur _end . line || cur _start . ch !== cur _end . ch ) {
// use selection
block _start = cur _start . line ;
block _end = cur _end . line ;
if ( cur _end . ch === 0 ) {
block _end -- ;
}
} else {
// no selection, search for ends of this indented block
for ( block _start = cur _start . line ; block _start >= 0 ; block _start -- ) {
line = cm . getLineHandle ( block _start ) ;
if ( line . text . match ( /^\s*$/ ) ) {
// empty or all whitespace - keep going
continue ;
} else {
if ( code _type ( cm , block _start , line ) !== 'indented' ) {
block _start += 1 ;
break ;
}
}
}
lineCount = cm . lineCount ( ) ;
for ( block _end = cur _start . line ; block _end < lineCount ; block _end ++ ) {
line = cm . getLineHandle ( block _end ) ;
if ( line . text . match ( /^\s*$/ ) ) {
// empty or all whitespace - keep going
continue ;
} else {
if ( code _type ( cm , block _end , line ) !== 'indented' ) {
block _end -= 1 ;
break ;
}
}
}
}
// if we are going to un-indent based on a selected set of lines, and the next line is indented too, we need to
// insert a blank line so that the next line(s) continue to be indented code
var next _line = cm . getLineHandle ( block _end + 1 ) ,
next _line _last _tok = next _line && cm . getTokenAt ( {
line : block _end + 1 ,
ch : next _line . text . length - 1
} ) ,
next _line _indented = next _line _last _tok && token _state ( next _line _last _tok ) . indentedCode ;
if ( next _line _indented ) {
cm . replaceRange ( '\n' , {
line : block _end + 1 ,
ch : 0
} ) ;
}
for ( var i = block _start ; i <= block _end ; i ++ ) {
cm . indentLine ( i , 'subtract' ) ; // TODO: this doesn't get tracked in the history, so can't be undone :(
}
cm . focus ( ) ;
} else {
// insert code formatting
var no _sel _and _starting _of _line = ( cur _start . line === cur _end . line && cur _start . ch === cur _end . ch && cur _start . ch === 0 ) ;
var sel _multi = cur _start . line !== cur _end . line ;
if ( no _sel _and _starting _of _line || sel _multi ) {
insertFencingAtSelection ( cm , cur _start , cur _end , fenceCharsToInsert ) ;
} else {
_replaceSelection ( cm , false , [ '`' , '`' ] ) ;
}
}
}
/ * *
* Action for toggling blockquote .
* /
function toggleBlockquote ( editor ) {
var cm = editor . codemirror ;
_toggleLine ( cm , 'quote' ) ;
}
/ * *
* Action for toggling heading size : normal - > h1 - > h2 - > h3 - > h4 - > h5 - > h6 - > normal
* /
function toggleHeadingSmaller ( editor ) {
var cm = editor . codemirror ;
_toggleHeading ( cm , 'smaller' ) ;
}
/ * *
* Action for toggling heading size : normal - > h6 - > h5 - > h4 - > h3 - > h2 - > h1 - > normal
* /
function toggleHeadingBigger ( editor ) {
var cm = editor . codemirror ;
_toggleHeading ( cm , 'bigger' ) ;
}
/ * *
* Action for toggling heading size 1
* /
function toggleHeading1 ( editor ) {
var cm = editor . codemirror ;
_toggleHeading ( cm , undefined , 1 ) ;
}
/ * *
* Action for toggling heading size 2
* /
function toggleHeading2 ( editor ) {
var cm = editor . codemirror ;
_toggleHeading ( cm , undefined , 2 ) ;
}
/ * *
* Action for toggling heading size 3
* /
function toggleHeading3 ( editor ) {
var cm = editor . codemirror ;
_toggleHeading ( cm , undefined , 3 ) ;
}
/ * *
* Action for toggling ul .
* /
function toggleUnorderedList ( editor ) {
var cm = editor . codemirror ;
_toggleLine ( cm , 'unordered-list' ) ;
}
/ * *
* Action for toggling ol .
* /
function toggleOrderedList ( editor ) {
var cm = editor . codemirror ;
_toggleLine ( cm , 'ordered-list' ) ;
}
/ * *
* Action for clean block ( remove headline , list , blockquote code , markers )
* /
function cleanBlock ( editor ) {
var cm = editor . codemirror ;
_cleanBlock ( cm ) ;
}
/ * *
* Action for drawing a link .
* /
function drawLink ( editor ) {
var cm = editor . codemirror ;
var stat = getState ( cm ) ;
var options = editor . options ;
var url = 'https://' ;
if ( options . promptURLs ) {
url = prompt ( options . promptTexts . link ) ;
if ( ! url ) {
return false ;
}
}
_replaceSelection ( cm , stat . link , options . insertTexts . link , url ) ;
}
/ * *
* Action for drawing an img .
* /
function drawImage ( editor ) {
var cm = editor . codemirror ;
var stat = getState ( cm ) ;
var options = editor . options ;
var url = 'https://' ;
if ( options . promptURLs ) {
url = prompt ( options . promptTexts . image ) ;
if ( ! url ) {
return false ;
}
}
_replaceSelection ( cm , stat . image , options . insertTexts . image , url ) ;
}
/ * *
* Action for drawing a table .
* /
function drawTable ( editor ) {
var cm = editor . codemirror ;
var stat = getState ( cm ) ;
var options = editor . options ;
_replaceSelection ( cm , stat . table , options . insertTexts . table ) ;
}
/ * *
* Action for drawing a horizontal rule .
* /
function drawHorizontalRule ( editor ) {
var cm = editor . codemirror ;
var stat = getState ( cm ) ;
var options = editor . options ;
_replaceSelection ( cm , stat . image , options . insertTexts . horizontalRule ) ;
}
/ * *
* Undo action .
* /
function undo ( editor ) {
var cm = editor . codemirror ;
cm . undo ( ) ;
cm . focus ( ) ;
}
/ * *
* Redo action .
* /
function redo ( editor ) {
var cm = editor . codemirror ;
cm . redo ( ) ;
cm . focus ( ) ;
}
/ * *
* Toggle side by side preview
* /
function toggleSideBySide ( editor ) {
var cm = editor . codemirror ;
var wrapper = cm . getWrapperElement ( ) ;
var preview = wrapper . nextSibling ;
var toolbarButton = editor . toolbarElements [ 'side-by-side' ] ;
var useSideBySideListener = false ;
if ( /editor-preview-active-side/ . test ( preview . className ) ) {
preview . className = preview . className . replace (
/\s*editor-preview-active-side\s*/g , ''
) ;
toolbarButton . className = toolbarButton . className . replace ( /\s*active\s*/g , '' ) ;
wrapper . className = wrapper . className . replace ( /\s*CodeMirror-sided\s*/g , ' ' ) ;
} else {
// When the preview button is clicked for the first time,
// give some time for the transition from editor.css to fire and the view to slide from right to left,
// instead of just appearing.
setTimeout ( function ( ) {
if ( ! cm . getOption ( 'fullScreen' ) )
toggleFullScreen ( editor ) ;
preview . className += ' editor-preview-active-side' ;
} , 1 ) ;
toolbarButton . className += ' active' ;
wrapper . className += ' CodeMirror-sided' ;
useSideBySideListener = true ;
}
// Hide normal preview if active
var previewNormal = wrapper . lastChild ;
if ( /editor-preview-active/ . test ( previewNormal . className ) ) {
previewNormal . className = previewNormal . className . replace (
/\s*editor-preview-active\s*/g , ''
) ;
var toolbar = editor . toolbarElements . preview ;
var toolbar _div = wrapper . previousSibling ;
toolbar . className = toolbar . className . replace ( /\s*active\s*/g , '' ) ;
toolbar _div . className = toolbar _div . className . replace ( /\s*disabled-for-preview*/g , '' ) ;
}
var sideBySideRenderingFunction = function ( ) {
preview . innerHTML = editor . options . previewRender ( editor . value ( ) , preview ) ;
} ;
if ( ! cm . sideBySideRenderingFunction ) {
cm . sideBySideRenderingFunction = sideBySideRenderingFunction ;
}
if ( useSideBySideListener ) {
preview . innerHTML = editor . options . previewRender ( editor . value ( ) , preview ) ;
cm . on ( 'update' , cm . sideBySideRenderingFunction ) ;
} else {
cm . off ( 'update' , cm . sideBySideRenderingFunction ) ;
}
// Refresh to fix selection being off (#309)
cm . refresh ( ) ;
}
/ * *
* Preview action .
* /
function togglePreview ( editor ) {
var cm = editor . codemirror ;
var wrapper = cm . getWrapperElement ( ) ;
var toolbar _div = wrapper . previousSibling ;
var toolbar = editor . options . toolbar ? editor . toolbarElements . preview : false ;
var preview = wrapper . lastChild ;
if ( ! preview || ! /editor-preview/ . test ( preview . className ) ) {
preview = document . createElement ( 'div' ) ;
preview . className = 'editor-preview' ;
wrapper . appendChild ( preview ) ;
}
if ( /editor-preview-active/ . test ( preview . className ) ) {
preview . className = preview . className . replace (
/\s*editor-preview-active\s*/g , ''
) ;
if ( toolbar ) {
toolbar . className = toolbar . className . replace ( /\s*active\s*/g , '' ) ;
toolbar _div . className = toolbar _div . className . replace ( /\s*disabled-for-preview*/g , '' ) ;
}
} else {
// When the preview button is clicked for the first time,
// give some time for the transition from editor.css to fire and the view to slide from right to left,
// instead of just appearing.
setTimeout ( function ( ) {
preview . className += ' editor-preview-active' ;
} , 1 ) ;
if ( toolbar ) {
toolbar . className += ' active' ;
toolbar _div . className += ' disabled-for-preview' ;
}
}
preview . innerHTML = editor . options . previewRender ( editor . value ( ) , preview ) ;
// Turn off side by side if needed
var sidebyside = cm . getWrapperElement ( ) . nextSibling ;
if ( /editor-preview-active-side/ . test ( sidebyside . className ) )
toggleSideBySide ( editor ) ;
}
function _replaceSelection ( cm , active , startEnd , url ) {
if ( /editor-preview-active/ . test ( cm . getWrapperElement ( ) . lastChild . className ) )
return ;
var text ;
var start = startEnd [ 0 ] ;
var end = startEnd [ 1 ] ;
var startPoint = { } ,
endPoint = { } ;
Object . assign ( startPoint , cm . getCursor ( 'start' ) ) ;
Object . assign ( endPoint , cm . getCursor ( 'end' ) ) ;
if ( url ) {
end = end . replace ( '#url#' , url ) ;
}
if ( active ) {
text = cm . getLine ( startPoint . line ) ;
start = text . slice ( 0 , startPoint . ch ) ;
end = text . slice ( startPoint . ch ) ;
cm . replaceRange ( start + end , {
line : startPoint . line ,
ch : 0
} ) ;
} else {
text = cm . getSelection ( ) ;
cm . replaceSelection ( start + text + end ) ;
startPoint . ch += start . length ;
if ( startPoint !== endPoint ) {
endPoint . ch += start . length ;
}
}
cm . setSelection ( startPoint , endPoint ) ;
cm . focus ( ) ;
}
function _toggleHeading ( cm , direction , size ) {
if ( /editor-preview-active/ . test ( cm . getWrapperElement ( ) . lastChild . className ) )
return ;
var startPoint = cm . getCursor ( 'start' ) ;
var endPoint = cm . getCursor ( 'end' ) ;
for ( var i = startPoint . line ; i <= endPoint . line ; i ++ ) {
( function ( i ) {
var text = cm . getLine ( i ) ;
var currHeadingLevel = text . search ( /[^#]/ ) ;
if ( direction !== undefined ) {
if ( currHeadingLevel <= 0 ) {
if ( direction == 'bigger' ) {
text = '###### ' + text ;
} else {
text = '# ' + text ;
}
} else if ( currHeadingLevel == 6 && direction == 'smaller' ) {
text = text . substr ( 7 ) ;
} else if ( currHeadingLevel == 1 && direction == 'bigger' ) {
text = text . substr ( 2 ) ;
} else {
if ( direction == 'bigger' ) {
text = text . substr ( 1 ) ;
} else {
text = '#' + text ;
}
}
} else {
if ( size == 1 ) {
if ( currHeadingLevel <= 0 ) {
text = '# ' + text ;
} else if ( currHeadingLevel == size ) {
text = text . substr ( currHeadingLevel + 1 ) ;
} else {
text = '# ' + text . substr ( currHeadingLevel + 1 ) ;
}
} else if ( size == 2 ) {
if ( currHeadingLevel <= 0 ) {
text = '## ' + text ;
} else if ( currHeadingLevel == size ) {
text = text . substr ( currHeadingLevel + 1 ) ;
} else {
text = '## ' + text . substr ( currHeadingLevel + 1 ) ;
}
} else {
if ( currHeadingLevel <= 0 ) {
text = '### ' + text ;
} else if ( currHeadingLevel == size ) {
text = text . substr ( currHeadingLevel + 1 ) ;
} else {
text = '### ' + text . substr ( currHeadingLevel + 1 ) ;
}
}
}
cm . replaceRange ( text , {
line : i ,
ch : 0
} , {
line : i ,
ch : 99999999999999
} ) ;
} ) ( i ) ;
}
cm . focus ( ) ;
}
function _toggleLine ( cm , name ) {
if ( /editor-preview-active/ . test ( cm . getWrapperElement ( ) . lastChild . className ) )
return ;
var listRegexp = /^(\s*)(\*|-|\+|\d*\.)(\s+)/ ;
var whitespacesRegexp = /^\s*/ ;
var stat = getState ( cm ) ;
var startPoint = cm . getCursor ( 'start' ) ;
var endPoint = cm . getCursor ( 'end' ) ;
var repl = {
'quote' : /^(\s*)>\s+/ ,
'unordered-list' : listRegexp ,
'ordered-list' : listRegexp
} ;
var _getChar = function ( name , i ) {
var map = {
'quote' : '>' ,
'unordered-list' : '*' ,
'ordered-list' : '%%i.'
} ;
return map [ name ] . replace ( '%%i' , i ) ;
} ;
var _checkChar = function ( name , char ) {
var map = {
'quote' : '>' ,
'unordered-list' : '*' ,
'ordered-list' : 'd+.'
} ;
var rt = new RegExp ( map [ name ] ) ;
return char && rt . test ( char ) ;
} ;
var line = 1 ;
for ( var i = startPoint . line ; i <= endPoint . line ; i ++ ) {
( function ( i ) {
var text = cm . getLine ( i ) ;
if ( stat [ name ] ) {
text = text . replace ( repl [ name ] , '$1' ) ;
} else {
var arr = listRegexp . exec ( text ) ;
var char = _getChar ( name , line ) ;
if ( arr !== null ) {
if ( _checkChar ( name , arr [ 2 ] ) ) {
char = '' ;
}
text = arr [ 1 ] + char + arr [ 3 ] + text . replace ( whitespacesRegexp , '' ) . replace ( repl [ name ] , '$1' ) ;
} else {
text = char + ' ' + text ;
}
line += 1 ;
}
cm . replaceRange ( text , {
line : i ,
ch : 0
} , {
line : i ,
ch : 99999999999999
} ) ;
} ) ( i ) ;
}
cm . focus ( ) ;
}
function _toggleBlock ( editor , type , start _chars , end _chars ) {
if ( /editor-preview-active/ . test ( editor . codemirror . getWrapperElement ( ) . lastChild . className ) )
return ;
end _chars = ( typeof end _chars === 'undefined' ) ? start _chars : end _chars ;
var cm = editor . codemirror ;
var stat = getState ( cm ) ;
var text ;
var start = start _chars ;
var end = end _chars ;
var startPoint = cm . getCursor ( 'start' ) ;
var endPoint = cm . getCursor ( 'end' ) ;
if ( stat [ type ] ) {
text = cm . getLine ( startPoint . line ) ;
start = text . slice ( 0 , startPoint . ch ) ;
end = text . slice ( startPoint . ch ) ;
if ( type == 'bold' ) {
start = start . replace ( /(\*\*|__)(?![\s\S]*(\*\*|__))/ , '' ) ;
end = end . replace ( /(\*\*|__)/ , '' ) ;
} else if ( type == 'italic' ) {
start = start . replace ( /(\*|_)(?![\s\S]*(\*|_))/ , '' ) ;
end = end . replace ( /(\*|_)/ , '' ) ;
} else if ( type == 'strikethrough' ) {
start = start . replace ( /(\*\*|~~)(?![\s\S]*(\*\*|~~))/ , '' ) ;
end = end . replace ( /(\*\*|~~)/ , '' ) ;
}
cm . replaceRange ( start + end , {
line : startPoint . line ,
ch : 0
} , {
line : startPoint . line ,
ch : 99999999999999
} ) ;
if ( type == 'bold' || type == 'strikethrough' ) {
startPoint . ch -= 2 ;
if ( startPoint !== endPoint ) {
endPoint . ch -= 2 ;
}
} else if ( type == 'italic' ) {
startPoint . ch -= 1 ;
if ( startPoint !== endPoint ) {
endPoint . ch -= 1 ;
}
}
} else {
text = cm . getSelection ( ) ;
if ( type == 'bold' ) {
text = text . split ( '**' ) . join ( '' ) ;
text = text . split ( '__' ) . join ( '' ) ;
} else if ( type == 'italic' ) {
text = text . split ( '*' ) . join ( '' ) ;
text = text . split ( '_' ) . join ( '' ) ;
} else if ( type == 'strikethrough' ) {
text = text . split ( '~~' ) . join ( '' ) ;
}
cm . replaceSelection ( start + text + end ) ;
startPoint . ch += start _chars . length ;
endPoint . ch = startPoint . ch + text . length ;
}
cm . setSelection ( startPoint , endPoint ) ;
cm . focus ( ) ;
}
function _cleanBlock ( cm ) {
if ( /editor-preview-active/ . test ( cm . getWrapperElement ( ) . lastChild . className ) )
return ;
var startPoint = cm . getCursor ( 'start' ) ;
var endPoint = cm . getCursor ( 'end' ) ;
var text ;
for ( var line = startPoint . line ; line <= endPoint . line ; line ++ ) {
text = cm . getLine ( line ) ;
text = text . replace ( /^[ ]*([# ]+|\*|-|[> ]+|[0-9]+(.|\)))[ ]*/ , '' ) ;
cm . replaceRange ( text , {
line : line ,
ch : 0
} , {
line : line ,
ch : 99999999999999
} ) ;
}
}
// Merge the properties of one object into another.
function _mergeProperties ( target , source ) {
for ( var property in source ) {
if ( source . hasOwnProperty ( property ) ) {
if ( source [ property ] instanceof Array ) {
target [ property ] = source [ property ] . concat ( target [ property ] instanceof Array ? target [ property ] : [ ] ) ;
} else if (
source [ property ] !== null &&
typeof source [ property ] === 'object' &&
source [ property ] . constructor === Object
) {
target [ property ] = _mergeProperties ( target [ property ] || { } , source [ property ] ) ;
} else {
target [ property ] = source [ property ] ;
}
}
}
return target ;
}
// Merge an arbitrary number of objects into one.
function extend ( target ) {
for ( var i = 1 ; i < arguments . length ; i ++ ) {
target = _mergeProperties ( target , arguments [ i ] ) ;
}
return target ;
}
/* The right word count in respect for CJK. */
function wordCount ( data ) {
var pattern = /[a-zA-Z0-9_\u0392-\u03c9\u0410-\u04F9]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g ;
var m = data . match ( pattern ) ;
var count = 0 ;
if ( m === null ) return count ;
for ( var i = 0 ; i < m . length ; i ++ ) {
if ( m [ i ] . charCodeAt ( 0 ) >= 0x4E00 ) {
count += m [ i ] . length ;
} else {
count += 1 ;
}
}
return count ;
}
var toolbarBuiltInButtons = {
'bold' : {
name : 'bold' ,
action : toggleBold ,
className : 'fa fa-bold' ,
title : 'Bold' ,
default : true
} ,
'italic' : {
name : 'italic' ,
action : toggleItalic ,
className : 'fa fa-italic' ,
title : 'Italic' ,
default : true
} ,
'strikethrough' : {
name : 'strikethrough' ,
action : toggleStrikethrough ,
className : 'fa fa-strikethrough' ,
title : 'Strikethrough'
} ,
'heading' : {
name : 'heading' ,
action : toggleHeadingSmaller ,
className : 'fa fa-header fa-heading' ,
title : 'Heading' ,
default : true
} ,
'heading-smaller' : {
name : 'heading-smaller' ,
action : toggleHeadingSmaller ,
className : 'fa fa-header fa-heading fa-header-x fa-header-smaller' ,
title : 'Smaller Heading'
} ,
'heading-bigger' : {
name : 'heading-bigger' ,
action : toggleHeadingBigger ,
className : 'fa fa-header fa-heading fa-header-x fa-header-bigger' ,
title : 'Bigger Heading'
} ,
'heading-1' : {
name : 'heading-1' ,
action : toggleHeading1 ,
className : 'fa fa-header fa-heading fa-header-x fa-header-1' ,
title : 'Big Heading'
} ,
'heading-2' : {
name : 'heading-2' ,
action : toggleHeading2 ,
className : 'fa fa-header fa-heading fa-header-x fa-header-2' ,
title : 'Medium Heading'
} ,
'heading-3' : {
name : 'heading-3' ,
action : toggleHeading3 ,
className : 'fa fa-header fa-heading fa-header-x fa-header-3' ,
title : 'Small Heading'
} ,
'separator-1' : {
name : 'separator-1'
} ,
'code' : {
name : 'code' ,
action : toggleCodeBlock ,
className : 'fa fa-code' ,
title : 'Code'
} ,
'quote' : {
name : 'quote' ,
action : toggleBlockquote ,
className : 'fa fa-quote-left' ,
title : 'Quote' ,
default : true
} ,
'unordered-list' : {
name : 'unordered-list' ,
action : toggleUnorderedList ,
className : 'fa fa-list-ul' ,
title : 'Generic List' ,
default : true
} ,
'ordered-list' : {
name : 'ordered-list' ,
action : toggleOrderedList ,
className : 'fa fa-list-ol' ,
title : 'Numbered List' ,
default : true
} ,
'clean-block' : {
name : 'clean-block' ,
action : cleanBlock ,
className : 'fa fa-eraser fa-clean-block' ,
title : 'Clean block'
} ,
'separator-2' : {
name : 'separator-2'
} ,
'link' : {
name : 'link' ,
action : drawLink ,
className : 'fa fa-link' ,
title : 'Create Link' ,
default : true
} ,
'image' : {
name : 'image' ,
action : drawImage ,
className : 'fa fa-image' ,
title : 'Insert Image' ,
default : true
} ,
'table' : {
name : 'table' ,
action : drawTable ,
className : 'fa fa-table' ,
title : 'Insert Table'
} ,
'horizontal-rule' : {
name : 'horizontal-rule' ,
action : drawHorizontalRule ,
className : 'fa fa-minus' ,
title : 'Insert Horizontal Line'
} ,
'separator-3' : {
name : 'separator-3'
} ,
'preview' : {
name : 'preview' ,
action : togglePreview ,
className : 'fa fa-eye' ,
noDisable : true ,
title : 'Toggle Preview' ,
default : true
} ,
'side-by-side' : {
name : 'side-by-side' ,
action : toggleSideBySide ,
className : 'fa fa-columns' ,
noDisable : true ,
noMobile : true ,
title : 'Toggle Side by Side' ,
default : true
} ,
'fullscreen' : {
name : 'fullscreen' ,
action : toggleFullScreen ,
className : 'fa fa-arrows-alt' ,
noDisable : true ,
noMobile : true ,
title : 'Toggle Fullscreen' ,
default : true
} ,
'separator-4' : {
name : 'separator-4'
} ,
'guide' : {
name : 'guide' ,
action : 'https://simplemde.com/markdown-guide' ,
className : 'fa fa-question-circle' ,
noDisable : true ,
title : 'Markdown Guide' ,
default : true
} ,
'separator-5' : {
name : 'separator-5'
} ,
'undo' : {
name : 'undo' ,
action : undo ,
className : 'fa fa-undo' ,
noDisable : true ,
title : 'Undo'
} ,
'redo' : {
name : 'redo' ,
action : redo ,
className : 'fa fa-repeat' ,
noDisable : true ,
title : 'Redo'
}
} ;
var insertTexts = {
link : [ '[' , '](#url#)' ] ,
image : [ '![](' , '#url#)' ] ,
table : [ '' , '\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n' ] ,
horizontalRule : [ '' , '\n\n-----\n\n' ]
} ;
var promptTexts = {
link : 'URL for the link:' ,
image : 'URL of the image:'
} ;
var blockStyles = {
'bold' : '**' ,
'code' : '```' ,
'italic' : '*'
} ;
/ * *
* Interface of EasyMDE .
* /
function EasyMDE ( options ) {
// Handle options parameter
options = options || { } ;
// Used later to refer to it"s parent
options . parent = this ;
// Check if Font Awesome needs to be auto downloaded
var autoDownloadFA = true ;
if ( options . autoDownloadFontAwesome === false ) {
autoDownloadFA = false ;
}
if ( options . autoDownloadFontAwesome !== true ) {
var styleSheets = document . styleSheets ;
for ( var i = 0 ; i < styleSheets . length ; i ++ ) {
if ( ! styleSheets [ i ] . href )
continue ;
if ( styleSheets [ i ] . href . indexOf ( '//maxcdn.bootstrapcdn.com/font-awesome/' ) > - 1 ) {
autoDownloadFA = false ;
}
}
}
if ( autoDownloadFA ) {
var link = document . createElement ( 'link' ) ;
link . rel = 'stylesheet' ;
link . href = 'https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css' ;
document . getElementsByTagName ( 'head' ) [ 0 ] . appendChild ( link ) ;
}
// Find the textarea to use
if ( options . element ) {
this . element = options . element ;
} else if ( options . element === null ) {
// This means that the element option was specified, but no element was found
console . log ( 'EasyMDE: Error. No element was found.' ) ;
return ;
}
// Handle toolbar
if ( options . toolbar === undefined ) {
// Initialize
options . toolbar = [ ] ;
// Loop over the built in buttons, to get the preferred order
for ( var key in toolbarBuiltInButtons ) {
if ( toolbarBuiltInButtons . hasOwnProperty ( key ) ) {
if ( key . indexOf ( 'separator-' ) != - 1 ) {
options . toolbar . push ( '|' ) ;
}
if ( toolbarBuiltInButtons [ key ] . default === true || ( options . showIcons && options . showIcons . constructor === Array && options . showIcons . indexOf ( key ) != - 1 ) ) {
options . toolbar . push ( key ) ;
}
}
}
}
// Handle status bar
if ( ! options . hasOwnProperty ( 'status' ) ) {
options . status = [ 'autosave' , 'lines' , 'words' , 'cursor' ] ;
}
// Add default preview rendering function
if ( ! options . previewRender ) {
options . previewRender = function ( plainText ) {
// Note: "this" refers to the options object
return this . parent . markdown ( plainText ) ;
} ;
}
// Set default options for parsing config
options . parsingConfig = extend ( {
highlightFormatting : true // needed for toggleCodeBlock to detect types of code
} , options . parsingConfig || { } ) ;
// Merging the insertTexts, with the given options
options . insertTexts = extend ( { } , insertTexts , options . insertTexts || { } ) ;
// Merging the promptTexts, with the given options
options . promptTexts = extend ( { } , promptTexts , options . promptTexts || { } ) ;
// Merging the blockStyles, with the given options
options . blockStyles = extend ( { } , blockStyles , options . blockStyles || { } ) ;
// Merging the shortcuts, with the given options
options . shortcuts = extend ( { } , shortcuts , options . shortcuts || { } ) ;
options . minHeight = options . minHeight || '300px' ;
// Change unique_id to uniqueId for backwards compatibility
if ( options . autosave != undefined && options . autosave . unique _id != undefined && options . autosave . unique _id != '' )
options . autosave . uniqueId = options . autosave . unique _id ;
// Update this options
this . options = options ;
// Auto render
this . render ( ) ;
// The codemirror component is only available after rendering
// so, the setter for the initialValue can only run after
// the element has been rendered
if ( options . initialValue && ( ! this . options . autosave || this . options . autosave . foundSavedValue !== true ) ) {
this . value ( options . initialValue ) ;
}
}
/ * *
* Default markdown render .
* /
EasyMDE . prototype . markdown = function ( text ) {
if ( marked ) {
// Initialize
var markedOptions ;
if ( this . options && this . options . renderingConfig && this . options . renderingConfig . markedOptions ) {
markedOptions = this . options . renderingConfig . markedOptions ;
} else {
markedOptions = { } ;
}
// Update options
if ( this . options && this . options . renderingConfig && this . options . renderingConfig . singleLineBreaks === false ) {
markedOptions . breaks = false ;
} else {
markedOptions . breaks = true ;
}
if ( this . options && this . options . renderingConfig && this . options . renderingConfig . codeSyntaxHighlighting === true ) {
/* Get HLJS from config or window */
var hljs = this . options . renderingConfig . hljs || window . hljs ;
/* Check if HLJS loaded */
if ( hljs ) {
markedOptions . highlight = function ( code ) {
return hljs . highlightAuto ( code ) . value ;
} ;
}
}
// Set options
marked . setOptions ( markedOptions ) ;
// Convert the markdown to HTML
var htmlText = marked ( text ) ;
// Edit the HTML anchors to add 'target="_blank"' by default.
htmlText = addAnchorTargetBlank ( htmlText ) ;
return htmlText ;
}
} ;
/ * *
* Render editor to the given element .
* /
EasyMDE . prototype . render = function ( el ) {
if ( ! el ) {
el = this . element || document . getElementsByTagName ( 'textarea' ) [ 0 ] ;
}
if ( this . _rendered && this . _rendered === el ) {
// Already rendered.
return ;
}
this . element = el ;
var options = this . options ;
var self = this ;
var keyMaps = { } ;
for ( var key in options . shortcuts ) {
// null stands for "do not bind this command"
if ( options . shortcuts [ key ] !== null && bindings [ key ] !== null ) {
( function ( key ) {
keyMaps [ fixShortcut ( options . shortcuts [ key ] ) ] = function ( ) {
bindings [ key ] ( self ) ;
} ;
} ) ( key ) ;
}
}
keyMaps [ 'Enter' ] = 'newlineAndIndentContinueMarkdownList' ;
keyMaps [ 'Tab' ] = 'tabAndIndentMarkdownList' ;
keyMaps [ 'Shift-Tab' ] = 'shiftTabAndUnindentMarkdownList' ;
keyMaps [ 'Esc' ] = function ( cm ) {
if ( cm . getOption ( 'fullScreen' ) ) toggleFullScreen ( self ) ;
} ;
document . addEventListener ( 'keydown' , function ( e ) {
e = e || window . event ;
if ( e . keyCode == 27 ) {
if ( self . codemirror . getOption ( 'fullScreen' ) ) toggleFullScreen ( self ) ;
}
} , false ) ;
var mode , backdrop ;
if ( options . spellChecker !== false ) {
mode = 'spell-checker' ;
backdrop = options . parsingConfig ;
backdrop . name = 'gfm' ;
backdrop . gitHubSpice = false ;
CodeMirrorSpellChecker ( {
codeMirrorInstance : CodeMirror
} ) ;
} else {
mode = options . parsingConfig ;
mode . name = 'gfm' ;
mode . gitHubSpice = false ;
}
// eslint-disable-next-line no-unused-vars
function configureMouse ( cm , repeat , event ) {
return {
addNew : false
} ;
}
this . codemirror = CodeMirror . fromTextArea ( el , {
mode : mode ,
backdrop : backdrop ,
theme : 'paper' ,
tabSize : ( options . tabSize != undefined ) ? options . tabSize : 2 ,
indentUnit : ( options . tabSize != undefined ) ? options . tabSize : 2 ,
indentWithTabs : ( options . indentWithTabs === false ) ? false : true ,
lineNumbers : false ,
autofocus : ( options . autofocus === true ) ? true : false ,
extraKeys : keyMaps ,
lineWrapping : ( options . lineWrapping === false ) ? false : true ,
allowDropFileTypes : [ 'text/plain' ] ,
placeholder : options . placeholder || el . getAttribute ( 'placeholder' ) || '' ,
styleSelectedText : ( options . styleSelectedText != undefined ) ? options . styleSelectedText : ! isMobile ( ) ,
configureMouse : configureMouse
} ) ;
this . codemirror . getScrollerElement ( ) . style . minHeight = options . minHeight ;
if ( options . forceSync === true ) {
var cm = this . codemirror ;
cm . on ( 'change' , function ( ) {
cm . save ( ) ;
} ) ;
}
this . gui = { } ;
if ( options . toolbar !== false ) {
this . gui . toolbar = this . createToolbar ( ) ;
}
if ( options . status !== false ) {
this . gui . statusbar = this . createStatusbar ( ) ;
}
if ( options . autosave != undefined && options . autosave . enabled === true ) {
this . autosave ( ) ;
}
this . gui . sideBySide = this . createSideBySide ( ) ;
this . _rendered = this . element ;
// Fixes CodeMirror bug (#344)
var temp _cm = this . codemirror ;
setTimeout ( function ( ) {
temp _cm . refresh ( ) ;
} . bind ( temp _cm ) , 0 ) ;
} ;
// Safari, in Private Browsing Mode, looks like it supports localStorage but all calls to setItem throw QuotaExceededError. We're going to detect this and set a variable accordingly.
function isLocalStorageAvailable ( ) {
if ( typeof localStorage === 'object' ) {
try {
localStorage . setItem ( 'smde_localStorage' , 1 ) ;
localStorage . removeItem ( 'smde_localStorage' ) ;
} catch ( e ) {
return false ;
}
} else {
return false ;
}
return true ;
}
EasyMDE . prototype . autosave = function ( ) {
if ( isLocalStorageAvailable ( ) ) {
var easyMDE = this ;
if ( this . options . autosave . uniqueId == undefined || this . options . autosave . uniqueId == '' ) {
console . log ( 'EasyMDE: You must set a uniqueId to use the autosave feature' ) ;
return ;
}
if ( easyMDE . element . form != null && easyMDE . element . form != undefined ) {
easyMDE . element . form . addEventListener ( 'submit' , function ( ) {
localStorage . removeItem ( 'smde_' + easyMDE . options . autosave . uniqueId ) ;
} ) ;
}
if ( this . options . autosave . loaded !== true ) {
if ( typeof localStorage . getItem ( 'smde_' + this . options . autosave . uniqueId ) == 'string' && localStorage . getItem ( 'smde_' + this . options . autosave . uniqueId ) != '' ) {
this . codemirror . setValue ( localStorage . getItem ( 'smde_' + this . options . autosave . uniqueId ) ) ;
this . options . autosave . foundSavedValue = true ;
}
this . options . autosave . loaded = true ;
}
localStorage . setItem ( 'smde_' + this . options . autosave . uniqueId , easyMDE . value ( ) ) ;
var el = document . getElementById ( 'autosaved' ) ;
if ( el != null && el != undefined && el != '' ) {
var d = new Date ( ) ;
var hh = d . getHours ( ) ;
var m = d . getMinutes ( ) ;
var dd = 'am' ;
var h = hh ;
if ( h >= 12 ) {
h = hh - 12 ;
dd = 'pm' ;
}
if ( h == 0 ) {
h = 12 ;
}
m = m < 10 ? '0' + m : m ;
el . innerHTML = 'Autosaved: ' + h + ':' + m + ' ' + dd ;
}
this . autosaveTimeoutId = setTimeout ( function ( ) {
easyMDE . autosave ( ) ;
} , this . options . autosave . delay || 10000 ) ;
} else {
console . log ( 'EasyMDE: localStorage not available, cannot autosave' ) ;
}
} ;
EasyMDE . prototype . clearAutosavedValue = function ( ) {
if ( isLocalStorageAvailable ( ) ) {
if ( this . options . autosave == undefined || this . options . autosave . uniqueId == undefined || this . options . autosave . uniqueId == '' ) {
console . log ( 'EasyMDE: You must set a uniqueId to clear the autosave value' ) ;
return ;
}
localStorage . removeItem ( 'smde_' + this . options . autosave . uniqueId ) ;
} else {
console . log ( 'EasyMDE: localStorage not available, cannot autosave' ) ;
}
} ;
EasyMDE . prototype . createSideBySide = function ( ) {
var cm = this . codemirror ;
var wrapper = cm . getWrapperElement ( ) ;
var preview = wrapper . nextSibling ;
if ( ! preview || ! /editor-preview-side/ . test ( preview . className ) ) {
preview = document . createElement ( 'div' ) ;
preview . className = 'editor-preview-side' ;
wrapper . parentNode . insertBefore ( preview , wrapper . nextSibling ) ;
}
if ( this . options . syncSideBySidePreviewScroll === false ) return preview ;
// Syncs scroll editor -> preview
var cScroll = false ;
var pScroll = false ;
cm . on ( 'scroll' , function ( v ) {
if ( cScroll ) {
cScroll = false ;
return ;
}
pScroll = true ;
var height = v . getScrollInfo ( ) . height - v . getScrollInfo ( ) . clientHeight ;
var ratio = parseFloat ( v . getScrollInfo ( ) . top ) / height ;
var move = ( preview . scrollHeight - preview . clientHeight ) * ratio ;
preview . scrollTop = move ;
} ) ;
// Syncs scroll preview -> editor
preview . onscroll = function ( ) {
if ( pScroll ) {
pScroll = false ;
return ;
}
cScroll = true ;
var height = preview . scrollHeight - preview . clientHeight ;
var ratio = parseFloat ( preview . scrollTop ) / height ;
var move = ( cm . getScrollInfo ( ) . height - cm . getScrollInfo ( ) . clientHeight ) * ratio ;
cm . scrollTo ( 0 , move ) ;
} ;
return preview ;
} ;
EasyMDE . prototype . createToolbar = function ( items ) {
items = items || this . options . toolbar ;
if ( ! items || items . length === 0 ) {
return ;
}
var i ;
for ( i = 0 ; i < items . length ; i ++ ) {
if ( toolbarBuiltInButtons [ items [ i ] ] != undefined ) {
items [ i ] = toolbarBuiltInButtons [ items [ i ] ] ;
}
}
var bar = document . createElement ( 'div' ) ;
bar . className = 'editor-toolbar' ;
var self = this ;
var toolbarData = { } ;
self . toolbar = items ;
for ( i = 0 ; i < items . length ; i ++ ) {
if ( items [ i ] . name == 'guide' && self . options . toolbarGuideIcon === false )
continue ;
if ( self . options . hideIcons && self . options . hideIcons . indexOf ( items [ i ] . name ) != - 1 )
continue ;
// Fullscreen does not work well on mobile devices (even tablets)
// In the future, hopefully this can be resolved
if ( ( items [ i ] . name == 'fullscreen' || items [ i ] . name == 'side-by-side' ) && isMobile ( ) )
continue ;
// Don't include trailing separators
if ( items [ i ] === '|' ) {
var nonSeparatorIconsFollow = false ;
for ( var x = ( i + 1 ) ; x < items . length ; x ++ ) {
if ( items [ x ] !== '|' && ( ! self . options . hideIcons || self . options . hideIcons . indexOf ( items [ x ] . name ) == - 1 ) ) {
nonSeparatorIconsFollow = true ;
}
}
if ( ! nonSeparatorIconsFollow )
continue ;
}
// Create the icon and append to the toolbar
( function ( item ) {
var el ;
if ( item === '|' ) {
el = createSep ( ) ;
} else {
el = createIcon ( item , self . options . toolbarTips , self . options . shortcuts ) ;
}
// bind events, special for info
if ( item . action ) {
if ( typeof item . action === 'function' ) {
el . onclick = function ( e ) {
e . preventDefault ( ) ;
item . action ( self ) ;
} ;
} else if ( typeof item . action === 'string' ) {
el . onclick = function ( e ) {
e . preventDefault ( ) ;
window . open ( item . action , '_blank' ) ;
} ;
}
}
toolbarData [ item . name || item ] = el ;
bar . appendChild ( el ) ;
} ) ( items [ i ] ) ;
}
self . toolbarElements = toolbarData ;
var cm = this . codemirror ;
cm . on ( 'cursorActivity' , function ( ) {
var stat = getState ( cm ) ;
for ( var key in toolbarData ) {
( function ( key ) {
var el = toolbarData [ key ] ;
if ( stat [ key ] ) {
el . className += ' active' ;
} else if ( key != 'fullscreen' && key != 'side-by-side' ) {
el . className = el . className . replace ( /\s*active\s*/g , '' ) ;
}
} ) ( key ) ;
}
} ) ;
var cmWrapper = cm . getWrapperElement ( ) ;
cmWrapper . parentNode . insertBefore ( bar , cmWrapper ) ;
return bar ;
} ;
EasyMDE . prototype . createStatusbar = function ( status ) {
// Initialize
status = status || this . options . status ;
var options = this . options ;
var cm = this . codemirror ;
// Make sure the status variable is valid
if ( ! status || status . length === 0 )
return ;
// Set up the built-in items
var items = [ ] ;
var i , onUpdate , defaultValue ;
for ( i = 0 ; i < status . length ; i ++ ) {
// Reset some values
onUpdate = undefined ;
defaultValue = undefined ;
// Handle if custom or not
if ( typeof status [ i ] === 'object' ) {
items . push ( {
className : status [ i ] . className ,
defaultValue : status [ i ] . defaultValue ,
onUpdate : status [ i ] . onUpdate
} ) ;
} else {
var name = status [ i ] ;
if ( name === 'words' ) {
defaultValue = function ( el ) {
el . innerHTML = wordCount ( cm . getValue ( ) ) ;
} ;
onUpdate = function ( el ) {
el . innerHTML = wordCount ( cm . getValue ( ) ) ;
} ;
} else if ( name === 'lines' ) {
defaultValue = function ( el ) {
el . innerHTML = cm . lineCount ( ) ;
} ;
onUpdate = function ( el ) {
el . innerHTML = cm . lineCount ( ) ;
} ;
} else if ( name === 'cursor' ) {
defaultValue = function ( el ) {
el . innerHTML = '0:0' ;
} ;
onUpdate = function ( el ) {
var pos = cm . getCursor ( ) ;
el . innerHTML = pos . line + ':' + pos . ch ;
} ;
} else if ( name === 'autosave' ) {
defaultValue = function ( el ) {
if ( options . autosave != undefined && options . autosave . enabled === true ) {
el . setAttribute ( 'id' , 'autosaved' ) ;
}
} ;
}
items . push ( {
className : name ,
defaultValue : defaultValue ,
onUpdate : onUpdate
} ) ;
}
}
// Create element for the status bar
var bar = document . createElement ( 'div' ) ;
bar . className = 'editor-statusbar' ;
// Create a new span for each item
for ( i = 0 ; i < items . length ; i ++ ) {
// Store in temporary variable
var item = items [ i ] ;
// Create span element
var el = document . createElement ( 'span' ) ;
el . className = item . className ;
// Ensure the defaultValue is a function
if ( typeof item . defaultValue === 'function' ) {
item . defaultValue ( el ) ;
}
// Ensure the onUpdate is a function
if ( typeof item . onUpdate === 'function' ) {
// Create a closure around the span of the current action, then execute the onUpdate handler
this . codemirror . on ( 'update' , ( function ( el , item ) {
return function ( ) {
item . onUpdate ( el ) ;
} ;
} ( el , item ) ) ) ;
}
// Append the item to the status bar
bar . appendChild ( el ) ;
}
// Insert the status bar into the DOM
var cmWrapper = this . codemirror . getWrapperElement ( ) ;
cmWrapper . parentNode . insertBefore ( bar , cmWrapper . nextSibling ) ;
return bar ;
} ;
/ * *
* Get or set the text content .
* /
EasyMDE . prototype . value = function ( val ) {
var cm = this . codemirror ;
if ( val === undefined ) {
return cm . getValue ( ) ;
} else {
cm . getDoc ( ) . setValue ( val ) ;
if ( this . isPreviewActive ( ) ) {
var wrapper = cm . getWrapperElement ( ) ;
var preview = wrapper . lastChild ;
preview . innerHTML = this . options . previewRender ( val , preview ) ;
}
return this ;
}
} ;
/ * *
* Bind static methods for exports .
* /
EasyMDE . toggleBold = toggleBold ;
EasyMDE . toggleItalic = toggleItalic ;
EasyMDE . toggleStrikethrough = toggleStrikethrough ;
EasyMDE . toggleBlockquote = toggleBlockquote ;
EasyMDE . toggleHeadingSmaller = toggleHeadingSmaller ;
EasyMDE . toggleHeadingBigger = toggleHeadingBigger ;
EasyMDE . toggleHeading1 = toggleHeading1 ;
EasyMDE . toggleHeading2 = toggleHeading2 ;
EasyMDE . toggleHeading3 = toggleHeading3 ;
EasyMDE . toggleCodeBlock = toggleCodeBlock ;
EasyMDE . toggleUnorderedList = toggleUnorderedList ;
EasyMDE . toggleOrderedList = toggleOrderedList ;
EasyMDE . cleanBlock = cleanBlock ;
EasyMDE . drawLink = drawLink ;
EasyMDE . drawImage = drawImage ;
EasyMDE . drawTable = drawTable ;
EasyMDE . drawHorizontalRule = drawHorizontalRule ;
EasyMDE . undo = undo ;
EasyMDE . redo = redo ;
EasyMDE . togglePreview = togglePreview ;
EasyMDE . toggleSideBySide = toggleSideBySide ;
EasyMDE . toggleFullScreen = toggleFullScreen ;
/ * *
* Bind instance methods for exports .
* /
EasyMDE . prototype . toggleBold = function ( ) {
toggleBold ( this ) ;
} ;
EasyMDE . prototype . toggleItalic = function ( ) {
toggleItalic ( this ) ;
} ;
EasyMDE . prototype . toggleStrikethrough = function ( ) {
toggleStrikethrough ( this ) ;
} ;
EasyMDE . prototype . toggleBlockquote = function ( ) {
toggleBlockquote ( this ) ;
} ;
EasyMDE . prototype . toggleHeadingSmaller = function ( ) {
toggleHeadingSmaller ( this ) ;
} ;
EasyMDE . prototype . toggleHeadingBigger = function ( ) {
toggleHeadingBigger ( this ) ;
} ;
EasyMDE . prototype . toggleHeading1 = function ( ) {
toggleHeading1 ( this ) ;
} ;
EasyMDE . prototype . toggleHeading2 = function ( ) {
toggleHeading2 ( this ) ;
} ;
EasyMDE . prototype . toggleHeading3 = function ( ) {
toggleHeading3 ( this ) ;
} ;
EasyMDE . prototype . toggleCodeBlock = function ( ) {
toggleCodeBlock ( this ) ;
} ;
EasyMDE . prototype . toggleUnorderedList = function ( ) {
toggleUnorderedList ( this ) ;
} ;
EasyMDE . prototype . toggleOrderedList = function ( ) {
toggleOrderedList ( this ) ;
} ;
EasyMDE . prototype . cleanBlock = function ( ) {
cleanBlock ( this ) ;
} ;
EasyMDE . prototype . drawLink = function ( ) {
drawLink ( this ) ;
} ;
EasyMDE . prototype . drawImage = function ( ) {
drawImage ( this ) ;
} ;
EasyMDE . prototype . drawTable = function ( ) {
drawTable ( this ) ;
} ;
EasyMDE . prototype . drawHorizontalRule = function ( ) {
drawHorizontalRule ( this ) ;
} ;
EasyMDE . prototype . undo = function ( ) {
undo ( this ) ;
} ;
EasyMDE . prototype . redo = function ( ) {
redo ( this ) ;
} ;
EasyMDE . prototype . togglePreview = function ( ) {
togglePreview ( this ) ;
} ;
EasyMDE . prototype . toggleSideBySide = function ( ) {
toggleSideBySide ( this ) ;
} ;
EasyMDE . prototype . toggleFullScreen = function ( ) {
toggleFullScreen ( this ) ;
} ;
EasyMDE . prototype . isPreviewActive = function ( ) {
var cm = this . codemirror ;
var wrapper = cm . getWrapperElement ( ) ;
var preview = wrapper . lastChild ;
return /editor-preview-active/ . test ( preview . className ) ;
} ;
EasyMDE . prototype . isSideBySideActive = function ( ) {
var cm = this . codemirror ;
var wrapper = cm . getWrapperElement ( ) ;
var preview = wrapper . nextSibling ;
return /editor-preview-active-side/ . test ( preview . className ) ;
} ;
EasyMDE . prototype . isFullscreenActive = function ( ) {
var cm = this . codemirror ;
return cm . getOption ( 'fullScreen' ) ;
} ;
EasyMDE . prototype . getState = function ( ) {
var cm = this . codemirror ;
return getState ( cm ) ;
} ;
EasyMDE . prototype . toTextArea = function ( ) {
var cm = this . codemirror ;
var wrapper = cm . getWrapperElement ( ) ;
if ( wrapper . parentNode ) {
if ( this . gui . toolbar ) {
wrapper . parentNode . removeChild ( this . gui . toolbar ) ;
}
if ( this . gui . statusbar ) {
wrapper . parentNode . removeChild ( this . gui . statusbar ) ;
}
if ( this . gui . sideBySide ) {
wrapper . parentNode . removeChild ( this . gui . sideBySide ) ;
}
}
cm . toTextArea ( ) ;
if ( this . autosaveTimeoutId ) {
clearTimeout ( this . autosaveTimeoutId ) ;
this . autosaveTimeoutId = undefined ;
this . clearAutosavedValue ( ) ;
}
} ;
module . exports = EasyMDE ;