easymde
Version:
A simple, beautiful, and embeddable JavaScript Markdown editor that easy to use. Features include autosaving and spell checking.
1,506 lines (1,341 loc) • 102 kB
JavaScript
'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/display/autorefresh.js');
require('codemirror/addon/selection/mark-selection.js');
require('codemirror/addon/search/searchcursor.js');
require('codemirror/mode/gfm/gfm.js');
require('codemirror/mode/xml/xml.js');
var CodeMirrorSpellChecker = require('codemirror-spell-checker');
var marked = require('marked').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,
'toggleHeading4': toggleHeading4,
'toggleHeading5': toggleHeading5,
'toggleHeading6': toggleHeading6,
'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',
'toggleHeading1': 'Ctrl+Alt+1',
'toggleHeading2': 'Ctrl+Alt+2',
'toggleHeading3': 'Ctrl+Alt+3',
'toggleHeading4': 'Ctrl+Alt+4',
'toggleHeading5': 'Ctrl+Alt+5',
'toggleHeading6': 'Ctrl+Alt+6',
'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;
}
/**
* Modify HTML to remove the list-style when rendering checkboxes.
* @param {string} htmlText - HTML to be modified.
* @return {string} The modified HTML text.
*/
function removeListStyleWhenCheckbox(htmlText) {
var parser = new DOMParser();
var htmlDoc = parser.parseFromString(htmlText, 'text/html');
var listItems = htmlDoc.getElementsByTagName('li');
for (var i = 0; i < listItems.length; i++) {
var listItem = listItems[i];
for (var j = 0; j < listItem.children.length; j++) {
var listItemChild = listItem.children[j];
if (listItemChild instanceof HTMLInputElement && listItemChild.type === 'checkbox') {
// From Github: margin: 0 .2em .25em -1.6em;
listItem.style.marginLeft = '-1.5em';
listItem.style.listStyleType = 'none';
}
}
}
return htmlDoc.documentElement.innerHTML;
}
/**
* 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 dropdown block
*/
function createToolbarDropdown(options, enableTooltips, shortcuts, parent) {
var el = createToolbarButton(options, false, enableTooltips, shortcuts, 'button', parent);
el.classList.add('easymde-dropdown');
el.onclick = function () {
el.focus();
};
var content = document.createElement('div');
content.className = 'easymde-dropdown-content';
for (var childrenIndex = 0; childrenIndex < options.children.length; childrenIndex++) {
var child = options.children[childrenIndex];
var childElement;
if (typeof child === 'string' && child in toolbarBuiltInButtons) {
childElement = createToolbarButton(toolbarBuiltInButtons[child], true, enableTooltips, shortcuts, 'button', parent);
} else {
childElement = createToolbarButton(child, true, enableTooltips, shortcuts, 'button', parent);
}
childElement.addEventListener('click', function (e) { e.stopPropagation(); }, false);
content.appendChild(childElement);
}
el.appendChild(content);
return el;
}
/**
* Create button element for toolbar.
*/
function createToolbarButton(options, enableActions, enableTooltips, shortcuts, markup, parent) {
options = options || {};
var el = document.createElement(markup);
// Add 'custom' attributes as early as possible, so that 'official' attributes will never be overwritten.
if (options.attributes) {
for (var attribute in options.attributes) {
if (Object.prototype.hasOwnProperty.call(options.attributes, attribute)) {
el.setAttribute(attribute, options.attributes[attribute]);
}
}
}
var classNamePrefix = parent.options.toolbarButtonClassPrefix ? parent.options.toolbarButtonClassPrefix + '-' : '';
el.className = classNamePrefix + options.name;
el.setAttribute('type', markup);
enableTooltips = (enableTooltips == undefined) ? true : enableTooltips;
if (options.text) {
el.innerText = options.text;
}
// Properly handle custom shortcuts
if (options.name && options.name in shortcuts) {
bindings[options.name] = options.action;
}
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.title) {
el.setAttribute('aria-label', options.title);
}
if (options.noDisable) {
el.classList.add('no-disable');
}
if (options.noMobile) {
el.classList.add('no-mobile');
}
// Prevent errors if there is no class name in custom options
var classNameParts = [];
if (typeof options.className !== 'undefined') {
classNameParts = options.className.split(' ');
}
// Provide backwards compatibility with simple-markdown-editor by adding custom classes to the button.
var iconClasses = [];
for (var classNameIndex = 0; classNameIndex < classNameParts.length; classNameIndex++) {
var classNamePart = classNameParts[classNameIndex];
// Split icon classes from the button.
// Regex will detect "fa", "fas", "fa-something" and "fa-some-icon-1", but not "fanfare".
if (classNamePart.match(/^fa([srlb]|(-[\w-]*)|$)/)) {
iconClasses.push(classNamePart);
} else {
el.classList.add(classNamePart);
}
}
el.tabIndex = -1;
if (iconClasses.length > 0) {
// Create icon element and append as a child to the button
var icon = document.createElement('i');
for (var iconClassIndex = 0; iconClassIndex < iconClasses.length; iconClassIndex++) {
var iconClass = iconClasses[iconClassIndex];
icon.classList.add(iconClass);
}
el.appendChild(icon);
}
// If there is a custom icon markup set, use that
if (typeof options.icon !== 'undefined') {
el.innerHTML = options.icon;
}
if (options.action && enableActions) {
if (typeof options.action === 'function') {
el.onclick = function (e) {
e.preventDefault();
options.action(parent);
};
} else if (typeof options.action === 'string') {
el.onclick = function (e) {
e.preventDefault();
window.open(options.action, '_blank');
};
}
}
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.image) {
ret.link = true;
} else if (data === 'image') {
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.
* @param {EasyMDE} 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;
}
var wrapper = cm.getWrapperElement();
var sidebyside = wrapper.nextSibling;
if (sidebyside.classList.contains('editor-preview-active-side')) {
if (editor.options.sideBySideFullscreen === false) {
// if side-by-side not-fullscreen ok, apply classes as needed
var easyMDEContainer = wrapper.parentNode;
if (cm.getOption('fullScreen')) {
easyMDEContainer.classList.remove('sided--no-fullscreen');
} else {
easyMDEContainer.classList.add('sided--no-fullscreen');
}
} else {
toggleSideBySide(editor);
}
}
if (editor.options.onToggleFullScreen) {
editor.options.onToggleFullScreen(cm.getOption('fullScreen') || false);
}
// Remove or set maxHeight
if (typeof editor.options.maxHeight !== 'undefined') {
if (cm.getOption('fullScreen')) {
cm.getScrollerElement().style.removeProperty('height');
sidebyside.style.removeProperty('height');
} else {
cm.getScrollerElement().style.height = editor.options.maxHeight;
editor.setPreviewMaxHeight();
}
}
// Update toolbar class
editor.toolbar_div.classList.toggle('fullscreen');
// Update toolbar button
if (editor.toolbarElements && editor.toolbarElements.fullscreen) {
var toolbarButton = editor.toolbarElements.fullscreen;
toolbarButton.classList.toggle('active');
}
}
/**
* Action for toggling bold.
* @param {EasyMDE} editor
*/
function toggleBold(editor) {
_toggleBlock(editor, 'bold', editor.options.blockStyles.bold);
}
/**
* Action for toggling italic.
* @param {EasyMDE} editor
*/
function toggleItalic(editor) {
_toggleBlock(editor, 'italic', editor.options.blockStyles.italic);
}
/**
* Action for toggling strikethrough.
* @param {EasyMDE} editor
*/
function toggleStrikethrough(editor) {
_toggleBlock(editor, 'strikethrough', '~~');
}
/**
* Action for toggling code block.
* @param {EasyMDE} editor
*/
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) {
_toggleLine(editor.codemirror, 'quote');
}
/**
* Action for toggling heading size: normal -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 -> normal
*/
function toggleHeadingSmaller(editor) {
_toggleHeading(editor.codemirror, 'smaller');
}
/**
* Action for toggling heading size: normal -> h6 -> h5 -> h4 -> h3 -> h2 -> h1 -> normal
*/
function toggleHeadingBigger(editor) {
_toggleHeading(editor.codemirror, 'bigger');
}
/**
* Action for toggling heading size 1
*/
function toggleHeading1(editor) {
_toggleHeading(editor.codemirror, undefined, 1);
}
/**
* Action for toggling heading size 2
*/
function toggleHeading2(editor) {
_toggleHeading(editor.codemirror, undefined, 2);
}
/**
* Action for toggling heading size 3
*/
function toggleHeading3(editor) {
_toggleHeading(editor.codemirror, undefined, 3);
}
/**
* Action for toggling heading size 4
*/
function toggleHeading4(editor) {
_toggleHeading(editor.codemirror, undefined, 4);
}
/**
* Action for toggling heading size 5
*/
function toggleHeading5(editor) {
_toggleHeading(editor.codemirror, undefined, 5);
}
/**
* Action for toggling heading size 6
*/
function toggleHeading6(editor) {
_toggleHeading(editor.codemirror, undefined, 6);
}
/**
* Action for toggling ul.
*/
function toggleUnorderedList(editor) {
var cm = editor.codemirror;
var listStyle = '*'; // Default
if (['-', '+', '*'].includes(editor.options.unorderedListStyle)) {
listStyle = editor.options.unorderedListStyle;
}
_toggleLine(cm, 'unordered-list', listStyle);
}
/**
* Action for toggling ol.
*/
function toggleOrderedList(editor) {
_toggleLine(editor.codemirror, 'ordered-list');
}
/**
* Action for clean block (remove headline, list, blockquote code, markers)
*/
function cleanBlock(editor) {
_cleanBlock(editor.codemirror);
}
/**
* Action for drawing a link.
* @param {EasyMDE} editor
*/
function drawLink(editor) {
var options = editor.options;
var url = 'https://';
if (options.promptURLs) {
var result = prompt(options.promptTexts.link, url);
if (!result) {
return false;
}
url = escapePromptURL(result);
}
_toggleLink(editor, 'link', options.insertTexts.link, url);
}
/**
* Action for drawing an img.
* @param {EasyMDE} editor
*/
function drawImage(editor) {
var options = editor.options;
var url = 'https://';
if (options.promptURLs) {
var result = prompt(options.promptTexts.image, url);
if (!result) {
return false;
}
url = escapePromptURL(result);
}
_toggleLink(editor, 'image', options.insertTexts.image, url);
}
/**
* Encode and escape URLs to prevent breaking up rendered Markdown links.
* @param {string} url The url of the link or image
*/
function escapePromptURL(url) {
return encodeURI(url).replace(/([\\()])/g, '\\$1');
}
/**
* Action for opening the browse-file window to upload an image to a server.
* @param {EasyMDE} editor The EasyMDE object
*/
function drawUploadedImage(editor) {
// TODO: Draw the image template with a fake url? ie: ''
editor.openBrowseFileWindow();
}
/**
* Action executed after an image have been successfully imported on the server.
* @param {EasyMDE} editor The EasyMDE object
* @param {string} url The url of the uploaded image
*/
function afterImageUploaded(editor, url) {
var cm = editor.codemirror;
var stat = getState(cm);
var options = editor.options;
var imageName = url.substr(url.lastIndexOf('/') + 1);
var ext = imageName.substring(imageName.lastIndexOf('.') + 1).replace(/\?.*$/, '').toLowerCase();
// Check if media is an image
if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'apng', 'avif', 'webp'].includes(ext)) {
_replaceSelection(cm, stat.image, options.insertTexts.uploadedImage, url);
} else {
var text_link = options.insertTexts.link;
text_link[0] = '[' + imageName;
_replaceSelection(cm, stat.link, text_link, url);
}
// show uploaded image filename for 1000ms
editor.updateStatusBar('upload-image', editor.options.imageTexts.sbOnUploaded.replace('#image_name#', imageName));
setTimeout(function () {
editor.updateStatusBar('upload-image', editor.options.imageTexts.sbInit);
}, 1000);
}
/**
* Action for drawing a table.
* @param {EasyMDE} editor
*/
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.
* @param {EasyMDE} editor
*/
function drawHorizontalRule(editor) {
var cm = editor.codemirror;
var stat = getState(cm);
var options = editor.options;
_replaceSelection(cm, stat.image, options.insertTexts.horizontalRule);
}
/**
* Undo action.
* @param {EasyMDE} editor
*/
function undo(editor) {
var cm = editor.codemirror;
cm.undo();
cm.focus();
}
/**
* Redo action.
* @param {EasyMDE} editor
*/
function redo(editor) {
var cm = editor.codemirror;
cm.redo();
cm.focus();
}
/**
* Toggle side by side preview
* @param {EasyMDE} editor
*/
function toggleSideBySide(editor) {
var cm = editor.codemirror;
var wrapper = cm.getWrapperElement();
var preview = wrapper.nextSibling;
var toolbarButton = editor.toolbarElements && editor.toolbarElements['side-by-side'];
var useSideBySideListener = false;
var easyMDEContainer = wrapper.parentNode;
if (preview.classList.contains('editor-preview-active-side')) {
if (editor.options.sideBySideFullscreen === false) {
// if side-by-side not-fullscreen ok, remove classes when hiding side
easyMDEContainer.classList.remove('sided--no-fullscreen');
}
preview.classList.remove('editor-preview-active-side');
if (toolbarButton) toolbarButton.classList.remove('active');
wrapper.classList.remove('CodeMirror-sided');
} 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')) {
if (editor.options.sideBySideFullscreen === false) {
// if side-by-side not-fullscreen ok, add classes when not fullscreen and showing side
easyMDEContainer.classList.add('sided--no-fullscreen');
} else {
toggleFullScreen(editor);
}
}
preview.classList.add('editor-preview-active-side');
}, 1);
if (toolbarButton) toolbarButton.classList.add('active');
wrapper.classList.add('CodeMirror-sided');
useSideBySideListener = true;
}
// Hide normal preview if active
var previewNormal = wrapper.lastChild;
if (previewNormal.classList.contains('editor-preview-active')) {
previewNormal.classList.remove('editor-preview-active');
var toolbar = editor.toolbarElements.preview;
var toolbar_div = editor.toolbar_div;
toolbar.classList.remove('active');
toolbar_div.classList.remove('disabled-for-preview');
}
var sideBySideRenderingFunction = function () {
var newValue = editor.options.previewRender(editor.value(), preview);
if (newValue != null) {
preview.innerHTML = newValue;
}
};
if (!cm.sideBySideRenderingFunction) {
cm.sideBySideRenderingFunction = sideBySideRenderingFunction;
}
if (useSideBySideListener) {
var newValue = editor.options.previewRender(editor.value(), preview);
if (newValue != null) {
preview.innerHTML = newValue;
}
cm.on('update', cm.sideBySideRenderingFunction);
} else {
cm.off('update', cm.sideBySideRenderingFunction);
}
// Refresh to fix selection being off (#309)
cm.refresh();
}
/**
* Preview action.
* @param {EasyMDE} editor
*/
function togglePreview(editor) {
var cm = editor.codemirror;
var wrapper = cm.getWrapperElement();
var toolbar_div = editor.toolbar_div;
var toolbar = editor.options.toolbar ? editor.toolbarElements.preview : false;
var preview = wrapper.lastChild;
// Turn off side by side if needed
var sidebyside = cm.getWrapperElement().nextSibling;
if (sidebyside.classList.contains('editor-preview-active-side'))
toggleSideBySide(editor);
if (!preview || !preview.classList.contains('editor-preview-full')) {
preview = document.createElement('div');
preview.className = 'editor-preview-full';
if (editor.options.previewClass) {
if (Array.isArray(editor.options.previewClass)) {
for (var i = 0; i < editor.options.previewClass.length; i++) {
preview.classList.add(editor.options.previewClass[i]);
}
} else if (typeof editor.options.previewClass === 'string') {
preview.classList.add(editor.options.previewClass);
}
}
wrapper.appendChild(preview);
}
if (preview.classList.contains('editor-preview-active')) {
preview.classList.remove('editor-preview-active');
if (toolbar) {
toolbar.classList.remove('active');
toolbar_div.classList.remove('disabled-for-preview');
}
} 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.classList.add('editor-preview-active');
}, 1);
if (toolbar) {
toolbar.classList.add('active');
toolbar_div.classList.add('disabled-for-preview');
}
}
var preview_result = editor.options.previewRender(editor.value(), preview);
if (preview_result !== null) {
preview.innerHTML = preview_result;
}
}
function _replaceSelection(cm, active, startEnd, url) {
if (cm.getWrapperElement().lastChild.classList.contains('editor-preview-active'))
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) {
start = start.replace('#url#', url); // url is in start for upload-image
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 (cm.getWrapperElement().lastChild.classList.contains('editor-preview-active'))
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 (currHeadingLevel <= 0) {
text = '#'.repeat(size) + ' ' + text;
} else if (currHeadingLevel == size) {
text = text.substr(currHeadingLevel + 1);
} else {
text = '#'.repeat(size) + ' ' + text.substr(currHeadingLevel + 1);
}
}
cm.replaceRange(text, {
line: i,
ch: 0,
}, {
line: i,
ch: 99999999999999,
});
})(i);
}
cm.focus();
}
function _toggleLine(cm, name, liststyle) {
if (cm.getWrapperElement().lastChild.classList.contains('editor-preview-active'))
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': liststyle,
'ordered-list': '%%i.',
};
return map[name].replace('%%i', i);
};
var _checkChar = function (name, char) {
var map = {
'quote': '>',
'unordered-list': '\\' + liststyle,
'ordered-list': '\\d+.',
};
var rt = new RegExp(map[name]);
return char && rt.test(char);
};
var _toggle = function (name, text, untoggleOnly) {
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 if (untoggleOnly == false) {
text = char + ' ' + text;
}
return text;
};
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 {
// If we're toggling unordered-list formatting, check if the current line
// is part of an ordered-list, and if so, untoggle that first.
// Workaround for https://github.com/Ionaru/easy-markdown-editor/issues/92
if (name == 'unordered-list') {
text = _toggle('ordered-list', text, true);
}
text = _toggle(name, text, false);
line += 1;
}
cm.replaceRange(text, {
line: i,
ch: 0,
}, {
line: i,
ch: 99999999999999,
});
})(i);
}
cm.focus();
}
/**
* @param {EasyMDE} editor
* @param {'link' | 'image'} type
* @param {string} startEnd
* @param {string} url
*/
function _toggleLink(editor, type, startEnd, url) {
if (!editor.codemirror || editor.isPreviewActive()) {
return;
}
var cm = editor.codemirror;
var stat = getState(cm);
var active = stat[type];
if (!active) {
_replaceSelection(cm, active, startEnd, url);
return;
}
var startPoint = cm.getCursor('start');
var endPoint = cm.getCursor('end');
var text = cm.getLine(startPoint.line);
var start = text.slice(0, startPoint.ch);
var end = text.slice(startPoint.ch);
if (type == 'link') {
start = start.replace(/(.*)[^!]\[/, '$1');
} else if (type == 'image') {
start = start.replace(/(.*)!\[$/, '$1');
}
end = end.replace(/]\(.*?\)/, '');
cm.replaceRange(start + end, {
line: startPoint.line,
ch: 0,
}, {
line: startPoint.line,
ch: 99999999999999,
});
startPoint.ch -= startEnd[0].length;
if (startPoint !== endPoint) {
endPoint.ch -= startEnd[0].length;
}
cm.setSelection(startPoint, endPoint);
cm.focus();
}
/**
* @param {EasyMDE} editor
*/
function _toggleBlock(editor, type, start_chars, end_chars) {
if (!editor.codemirror || editor.isPreviewActive()) {
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 (cm.getWrapperElement().lastChild.classList.contains('editor-preview-active'))
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,
});
}
}
/**
* Convert a number of bytes to a human-readable file size. If you desire
* to add a space between the value and the unit, you need to add this space
* to the given units.
* @param bytes {number} A number of bytes, as integer. Ex: 421137
* @param units {number[]} An array of human-readable units, ie. [' B', ' K', ' MB']
* @returns string A human-readable file size. Ex: '412 KB'
*/
function humanFileSize(bytes, units) {
if (Math.abs(bytes) < 1024) {
return '' + bytes + units[0];
}
var u = 0;
do {
bytes /= 1024;
++u;
} while (Math.abs(bytes) >= 1024 && u < units.length);
return '' + bytes.toFixed(1) + units[u];
}
// Merge the properties of one object into another.
function _mergeProperties(target, source) {
for (var property in source) {
if (Object.prototype.hasOwnProperty.call(source, 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_\u00A0-\u02AF\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 iconClassMap = {
'bold': 'fa fa-bold',
'italic': 'fa fa-italic',
'strikethrough': 'fa fa-strikethrough',
'heading': 'fa fa-header fa-heading',
'heading-smaller': 'fa fa-header fa-heading header-smaller',
'heading-bigger': 'fa fa-header fa-heading header-bigger',
'heading-1': 'fa fa-header fa-heading header-1',
'heading-2': 'fa fa-header fa-heading header-2',
'heading-3': 'fa fa-header fa-heading header-3',
'code': 'fa fa-code',
'quote': 'fa fa-quote-left',
'ordered-list': 'fa fa-list-ol',
'unordered-list': 'fa fa-list-ul',
'clean-block': 'fa fa-eraser',
'link': 'fa fa-link',
'image': 'fa fa-image',
'upload-image': 'fa fa-image',
'table': 'fa fa-table',
'horizontal-rule': 'fa fa-minus',
'preview': 'fa fa-eye',
'side-by-side': 'fa fa-columns',
'fullscreen': 'fa fa-arrows-alt',
'guide': 'fa fa-question-circle',
'undo': 'fa fa-undo',
'redo': 'fa fa-repeat fa-redo',
};
var toolbarBuiltInButtons = {
'bold': {
name: 'bold',
action: toggleBold,
className: iconClassMap['bold'],
title: 'Bold',
default: true,
},
'italic': {
name: 'italic',
action: toggleItalic,
className: iconClassMap['italic'],
title: 'Italic',
default: true,
},
'strikethrough': {
name: 'strikethrough',
action: toggle