diff --git a/example/index_parsingConfig_headingLevels.html b/example/index_parsingConfig_headingLevels.html
new file mode 100644
index 00000000..587cffa0
--- /dev/null
+++ b/example/index_parsingConfig_headingLevels.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+ Example / Preview / parsingConfig / headingLevels
+
+
+
+
+
+
+
+
+
+
diff --git a/src/js/easymde.js b/src/js/easymde.js
index c038d1c7..d2117fb5 100644
--- a/src/js/easymde.js
+++ b/src/js/easymde.js
@@ -1124,6 +1124,13 @@ function _toggleHeading(cm, direction, size) {
var startPoint = cm.getCursor('start');
var endPoint = cm.getCursor('end');
+ var sharpLevels = cm.options.backdrop ? (cm.options.backdrop.headingLevels || []) : (cm.options.mode ? (cm.options.mode.headingLevels || []) : []),
+ minLevel = sharpLevels.length ? sharpLevels[0] : 1,
+ maxLevel = sharpLevels.length ? sharpLevels[sharpLevels.length-1] : 6;
+ if (size && sharpLevels.length && sharpLevels.indexOf(size) === -1) {
+ cm.focus();
+ return false;
+ }
for (var i = startPoint.line; i <= endPoint.line; i++) {
(function (i) {
var text = cm.getLine(i);
@@ -1132,14 +1139,20 @@ function _toggleHeading(cm, direction, size) {
if (direction !== undefined) {
if (currHeadingLevel <= 0) {
if (direction == 'bigger') {
- text = '###### ' + text;
+ text = '#'.repeat(maxLevel) + ' ' + text;
} else {
- text = '# ' + text;
+ text = '#'.repeat(minLevel) + ' ' + text;
+ }
+ } else if (currHeadingLevel == maxLevel && direction == 'smaller') {
+ text = text.substr(maxLevel + 1);
+ } else if (currHeadingLevel == minLevel && direction == 'bigger') {
+ text = text.substr(minLevel + 1);
+ } else if (sharpLevels.length) {
+ if (direction == 'bigger') {
+ text = '#'.repeat(sharpLevels[sharpLevels.indexOf(currHeadingLevel)-1]) + text.replace(/^[#]+/, '' );
+ } else {
+ text = '#'.repeat(sharpLevels[sharpLevels.indexOf(currHeadingLevel)+1]) + text.replace(/^[#]+/, '' );
}
- } 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);
@@ -1822,7 +1835,20 @@ function EasyMDE(options) {
options.parsingConfig = extend({
highlightFormatting: true, // needed for toggleCodeBlock to detect types of code
}, options.parsingConfig || {});
-
+ if ( options.parsingConfig.headingLevels ) {
+ var headingLevels = [];
+ for ( var l = 0, requestedLevels = options.parsingConfig.headingLevels; l < requestedLevels.length; l++ ) {
+ requestedLevels[ l ] = parseInt( requestedLevels[ l ], 10 );
+ if ( isNaN( requestedLevels[ l ] ) || headingLevels.indexOf( requestedLevels[ l ] ) !== -1 ) {
+ continue;
+ }
+ if ( requestedLevels[ l ] < 1 && requestedLevels[ l ] > 6 ) {
+ continue;
+ }
+ headingLevels[ l ] = requestedLevels[ l ];
+ }
+ options.parsingConfig.headingLevels = headingLevels.sort();
+ }
// Merging the insertTexts, with the given options
options.insertTexts = extend({}, insertTexts, options.insertTexts || {});
@@ -2190,6 +2216,410 @@ EasyMDE.prototype.render = function (el) {
cm.save();
});
}
+ if (options.parsingConfig.headingLevels) {
+ // If the *headingLevels* argument is present, set our custom modifiers
+ var headingMakeBigger = function(heading, from, to) {
+ heading = heading || '';
+ if (!from || !to || from >= to) {
+ return '';
+ }
+ var level = '';
+ if (!heading.length) {
+ while (from < to) {
+ level += '#';
+ from++;
+ }
+ level += ' ';
+ return level;
+ } else {
+ while (to > 0) {
+ level += '#';
+ to--;
+ }
+ level += ' ';
+ return /#/.test(heading) ? heading.replace(/^[#]+\s*/, level) : level;
+ }
+ };
+ var headingMakeSmaller = function(heading, from, to) {
+ heading = heading || '';
+ if (!from || !to || from <= to) {
+ return '';
+ }
+ var level = '';
+ if (!heading.length) {
+ while (from > to) {
+ level += '#';
+ from--;
+ }
+ level += ' ';
+ return level;
+ } else {
+ while (to > 0) {
+ level += '#';
+ to--;
+ }
+ level += ' ';
+ return /#/.test(heading) ? heading.replace(/^[#]+\s*/, level) : level;
+ }
+ };
+ var headingNeedUpdate = function(currHeading, allowedHeadingLevels, levelSearchDir) {
+ if (!currHeading || !allowedHeadingLevels) {
+ return false;
+ }
+ currHeading = currHeading.trim();
+ if (!/^#+/.test(currHeading)){
+ return false;
+ }
+ var currHeadingLevel = (currHeading.match(/^([#]+)/g) || [''])[0].length;
+ if (allowedHeadingLevels.indexOf(currHeadingLevel) !== -1) {
+ return false;
+ }
+ levelSearchDir = levelSearchDir || 'asc';
+ var newHeadingLevel = -1, n = 0;
+ if (levelSearchDir === 'asc') {
+ while (n < allowedHeadingLevels.length) {
+ if (allowedHeadingLevels[n] > currHeadingLevel) {
+ newHeadingLevel = allowedHeadingLevels[n];
+ break;
+ }
+ n++;
+ }
+ }
+ if (newHeadingLevel < 0) {
+ n = allowedHeadingLevels.length - 1;
+ while (n > -1) {
+ if (allowedHeadingLevels[n] < currHeadingLevel) {
+ newHeadingLevel = allowedHeadingLevels[n];
+ break;
+ }
+ n--;
+ }
+ if (levelSearchDir === 'dsc') {
+ currHeadingLevel += 1;
+ if (newHeadingLevel < 0) {
+ newHeadingLevel = 0;
+ }
+ }
+ }
+ if (newHeadingLevel < 0 || newHeadingLevel === currHeadingLevel) {
+ return false;
+ }
+ return {
+ from: currHeadingLevel,
+ to: newHeadingLevel,
+ diff: Math.abs(newHeadingLevel - currHeadingLevel),
+ };
+ };
+ var headingCheckNew = function(cm, obj) {
+ var currHeading = cm.getRange({
+ line: obj.from.line,
+ ch: 0,
+ }, {
+ line: obj.to.line,
+ ch: obj.to.ch,
+ });
+ var myLevels = headingNeedUpdate(currHeading, cm.options.backdrop ? cm.options.backdrop.headingLevels : cm.options.mode.headingLevels);
+ if (!myLevels || !myLevels.from || !myLevels.to) {
+ return false;
+ }
+ if (obj.from.line === obj.to.line) {
+ // Most simple case when a modification has occured on a single line
+ if (myLevels.to > myLevels.from) {
+ // Current level is forbidden so we jump to the closest upper level allowed
+ // We only need to reset the sharp numbers with the appropriate value before the modification is applied
+ obj.text[0] = headingMakeBigger('', myLevels.from, myLevels.to);
+ } else {
+ // The current level is forbidden and we jump to the closest lower level available
+ // A bit more is needed: we have to cancel the requested update and trigger a replacement with the existing sharp signs
+ obj.cancel();
+ var newHeading = '';
+ while (myLevels.to > 0) {
+ newHeading += '#';
+ myLevels.to--;
+ }
+ newHeading += ' ';
+ cm.doc.replaceRange(newHeading, {
+ line: obj.from.line,
+ ch: 0,
+ }, {
+ line: obj.to.line,
+ ch: obj.to.ch,
+ }, currHeading ); // 4th arguments to keep a trace in the history when possible
+ }
+ }
+ return true;
+ };
+ var headingCheckExisting = function(cm, obj) {
+ var myChar = cm.getRange({
+ line: obj.from.line,
+ ch: obj.from.ch,
+ }, {
+ line: obj.to.line,
+ ch: obj.to.ch + 1,
+ });
+ if (!/\s|#/.test(myChar || '')) {
+ // Don't bother to go further if no headling were detected
+ return false;
+ }
+ if ((obj.from.line === obj.to.line) && obj.text.length < 2) {
+ var myLevels, myText;
+ if (/input/.test(obj.origin) && obj.text[0] === '#') {
+ if (!/[^\s#]/.test(myText)) {
+ // Newly created, skip the check for now
+ return false;
+ }
+ myText = cm.getRange({
+ line: obj.from.line,
+ ch: 0,
+ }, {
+ line: obj.to.line,
+ ch: 8,
+ });
+ if (!/#/.test(myText)) {
+ myText = '# ' + myText.trim(); // Wasn't heading
+ } else {
+ myText = myText.replace(/#/, '##'); // Increment one sharp sign
+ }
+ myLevels = headingNeedUpdate(myText, cm.options.backdrop ? cm.options.backdrop.headingLevels : cm.options.mode.headingLevels);
+ if (!myLevels) {
+ return false;
+ }
+ if (myLevels.to < myLevels.from) {
+ obj.cancel();
+ return false;
+ }
+ obj.text[0] = '#';
+ while (myLevels.from < myLevels.to) {
+ obj.text[0] += '#';
+ myLevels.from++;
+ }
+ return true;
+ }
+ else if (/delete/.test(obj.origin) && obj.text[0] === '') {
+ var delChar = cm.getRange({
+ line: obj.from.line,
+ ch: obj.from.ch,
+ }, {
+ line: obj.to.line,
+ ch: obj.to.ch,
+ });
+ if (!delChar || !delChar.length) {
+ return false;
+ }
+ var searchDir = 'asc', myStart = {
+ line: obj.from.line,
+ ch: 0,
+ }, myEnd;
+ if (delChar.length === 1) {
+ myText = cm.getRange({line: obj.from.line, ch: 0}, {line: obj.to.line, ch: 8});
+ var myTextPart1 = myText.substring(0, obj.from.ch),
+ myTextPart2 = myText.substring(obj.from.ch + 1),
+ isPart1Heading = /^#/.test(myTextPart1) ? true : false,
+ isPart2Heading = /^#/.test(myTextPart2) ? true : false;
+ if (!isPart1Heading && !isPart2Heading) {
+ return false;
+ }
+ myText = myTextPart1 + myTextPart2;
+ searchDir = delChar === '#' ? 'dsc' : 'asc';
+ myEnd = {
+ line: obj.to.line,
+ ch: 8,
+ };
+ } else {
+ myText = cm.getRange({
+ line: obj.from.line,
+ ch: 0,
+ }, {
+ line: obj.to.line,
+ ch: obj.to.ch + 8,
+ });
+ myText = myText.replace(delChar, '');
+ myEnd = {
+ line: obj.to.line,
+ ch: obj.to.ch + 8,
+ };
+ }
+ myLevels = headingNeedUpdate(myText, cm.options.backdrop ? cm.options.backdrop.headingLevels : cm.options.mode.headingLevels, searchDir);
+ if (!myLevels || !myLevels.diff) {
+ return false;
+ }
+ obj.cancel();
+ obj.text[0] = '';
+ if (myLevels.to > 0) {
+ while (myLevels.to > 0) {
+ obj.text[0] += '#';
+ myLevels.to--;
+ }
+ }
+ if (delChar === '#') {
+ myLevels.from--;
+ }
+ var newText = myText.replace(new RegExp('^#' + '{' + myLevels.from + '}'), obj.text[0]);
+ // Just in case do not trim on both side, only trim the space that could remain next to the cursor
+ newText = newText.replace(/^\s*/, '');
+ cm.doc.replaceRange(newText, myStart, myEnd);
+ // Be gentle and set back the cursor at the appropriate position
+ cm.doc.setCursor({
+ line: obj.to.line,
+ ch: obj.text[0].length ? obj.text[0].length + 1 : 0,
+ });
+ return true;
+ }
+ }
+ };
+ var headingCheckRow = function(row, cm) {
+ if (!row || !/^#/.test(row.trim())) {
+ return row;
+ }
+ row = row.replace(/^(\s*)#/, '#');
+ var myLevels = headingNeedUpdate(row, cm.options.backdrop ? cm.options.backdrop.headingLevels : cm.options.mode.headingLevels);
+ if (!myLevels || !myLevels.from || !myLevels.to) {
+ return row;
+ } else if (myLevels.from < myLevels.to) {
+ return headingMakeBigger(row, myLevels.from, myLevels.to);
+ } else if (myLevels.to < myLevels.from) {
+ return headingMakeSmaller(row, myLevels.from, myLevels.to);
+ }
+ return row;
+ };
+ this.codemirror.on('beforeChange', function (cm, obj) {
+ // console.log(obj);
+ if (!obj || !obj.from || !obj.to || !obj.text || !obj.text.length) {
+ // Don't go further 'cause a required argument is missing...
+ return false;
+ }
+ if (!obj.origin) {
+ // If a modification was triggered by a code and not an human
+ // The origin can be "undefined", so just in case set one.
+ obj.origin = '+none';
+ }
+ if ((obj.from.line === obj.to.line) && obj.text.length < 2) {
+ if (!/delete/.test(obj.origin) && obj.to.ch > 6) {
+ // No need to trigger a check if we are on the same line at character 7 or upper
+ // As we are sure the cursor was not inside a range containing a sharp sign
+ return false;
+ }
+ if (obj.from.ch === 0 && obj.to.ch === 0 && /\s/.test(obj.text[0] || '')) {
+ // (Force) Prevent space at the beginning of the line
+ obj.cancel();
+ return false;
+ }
+ if (/input/.test(obj.origin)) { // Something was added
+ if (obj.text.length === 1 && obj.text[0].length === 1) {
+ // Only one character on one line is being updated
+ if (obj.text[0] === ' ') {
+ return headingCheckNew(cm, obj);
+ } else if (obj.text[0] === '#') {
+ return headingCheckExisting(cm, obj);
+ }
+ }
+ } else if (/delete/.test(obj.origin)) { // Something was removed
+ if (obj.text.length === 1 && !obj.text[0].length) {
+ // Only one character on one line has been removed
+ return headingCheckExisting(cm, obj);
+ }
+ }
+ }
+ // Multilines modification like a paste
+ var startText = '', endText = '', oldText = '', newText = '';
+ if (!/delete/.test(obj.origin)) {
+ if (obj.text.length < 2 && obj.text[0].length < 2) {
+ return false;
+ }
+ var r = 0, rEnd = obj.text.length; // Start row / End row
+ if (obj.from.ch > 7) {
+ // We are sure an new heading is not involved or conflicting with an existing one
+ // So we can safely exclude the first row from the verification loop
+ r = 1;
+ }
+ while (r < rEnd) {
+ if (!r && obj.from.ch > 0) {
+ // We need to check the first row in case an existing heading exists or is updated
+ startText = cm.getRange({
+ line: obj.from.line,
+ ch: 0,
+ }, {
+ line: obj.from.line,
+ ch: obj.from.ch,
+ });
+ if (/#/.test(startText)) {
+ oldText = startText + obj.text[r];
+ oldText = oldText.replace(/#\s+#/, '##');
+ newText = headingCheckRow(oldText, cm);
+ if (oldText !== newText) { // A modification has been made
+ obj.text[r] = newText.substring(obj.from.ch);
+ }
+ }
+ }
+ else if (r === rEnd - 1) {
+ endText = cm.getRange({
+ line: obj.from.line,
+ ch: obj.from.ch,
+ }, {
+ line: obj.from.line,
+ ch: obj.from.ch + 8,
+ });
+ if (/#/.test(endText)) {
+ oldText = obj.text[r] + endText;
+ oldText = oldText.replace(/#\s+#/, '##');
+ newText = headingCheckRow(oldText, cm);
+ if (oldText !== newText) { // A modification has been made
+ obj.text[r] = newText.replace(endText, '');
+ }
+ }
+ else {
+ obj.text[r] = headingCheckRow(obj.text[r], cm);
+ }
+ }
+ else { // 2nd and next rows
+ obj.text[r] = headingCheckRow(obj.text[r], cm);
+ }
+ r++;
+ }
+ } else {
+ // Multilines / multicharacters were removed
+ if (obj.from.ch > 7 || obj.text.length > 1 || obj.text[0].length > 1) {
+ return false;
+ }
+ startText = cm.getRange({
+ line: obj.from.line,
+ ch: 0,
+ }, {
+ line: obj.from.line,
+ ch: obj.from.ch,
+ });
+ endText = cm.getRange({
+ line: obj.to.line,
+ ch: obj.to.ch,
+ }, {
+ line: obj.to.line,
+ ch: obj.to.ch + 8,
+ });
+ oldText = startText + endText;
+ oldText = oldText.replace(/#\s+#/, '##');
+ if (/#/.test(oldText)) {
+ newText = headingCheckRow(oldText, cm);
+ if (oldText !== newText) { // A modification has been made
+ obj.cancel();
+ cm.doc.replaceRange('', {
+ line: obj.from.line,
+ ch: obj.from.ch,
+ }, {
+ line: obj.to.line,
+ ch: obj.to.ch,
+ });
+ cm.doc.replaceRange(newText, {
+ line: obj.from.line,
+ ch: 0,
+ }, {
+ line: obj.from.line,
+ ch: obj.from.ch + 8,
+ });
+ }
+ }
+ }
+ });
+ }
this.gui = {};