tiny-markdown-editor 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/TinyMDE.js CHANGED
@@ -32,18 +32,18 @@ class Editor {
32
32
  element = document.getElementById(props.element);
33
33
  }
34
34
  if (!element) {
35
- element = document.getElementsByTagName('body')[0];
35
+ element = document.getElementsByTagName("body")[0];
36
36
  }
37
- if (element.tagName == 'TEXTAREA') {
37
+ if (element.tagName == "TEXTAREA") {
38
38
  this.textarea = element;
39
39
  element = this.textarea.parentNode;
40
40
  }
41
41
  if (this.textarea) {
42
- this.textarea.style.display = 'none';
42
+ this.textarea.style.display = "none";
43
43
  }
44
44
  this.createEditorElement(element);
45
45
  // TODO Placeholder for empty content
46
- this.setContent(props.content || (this.textarea ? this.textarea.value : false) || '# Hello TinyMDE!\nEdit **here**');
46
+ this.setContent(props.content || (this.textarea ? this.textarea.value : "# Hello TinyMDE!\nEdit **here**"));
47
47
  }
48
48
 
49
49
  /**
@@ -51,24 +51,23 @@ class Editor {
51
51
  * @param element The target element of the DOM tree
52
52
  */
53
53
  createEditorElement(element) {
54
- this.e = document.createElement('div');
55
- this.e.className = 'TinyMDE';
54
+ this.e = document.createElement("div");
55
+ this.e.className = "TinyMDE";
56
56
  this.e.contentEditable = true;
57
57
  // The following is important for formatting purposes, but also since otherwise the browser replaces subsequent spaces with    
58
- // That breaks a lot of stuff, so we do this here and not in CSS—therefore, you don't have to remember to but this in the CSS file
59
- this.e.style.whiteSpace = 'pre-wrap';
58
+ // That breaks a lot of stuff, so we do this here and not in CSS—therefore, you don't have to remember to put this in the CSS file
59
+ this.e.style.whiteSpace = "pre-wrap";
60
60
  // Avoid formatting (B / I / U) popping up on iOS
61
- this.e.style.webkitUserModify = 'read-write-plaintext-only';
61
+ this.e.style.webkitUserModify = "read-write-plaintext-only";
62
62
  if (this.textarea && this.textarea.parentNode == element && this.textarea.nextSibling) {
63
63
  element.insertBefore(this.e, this.textarea.nextSibling);
64
64
  } else {
65
65
  element.appendChild(this.e);
66
66
  }
67
67
  this.e.addEventListener("input", e => this.handleInputEvent(e));
68
- // this.e.addEventListener("keydown", (e) => this.handleKeydownEvent(e));
68
+ this.e.addEventListener("compositionend", e => this.handleInputEvent(e));
69
69
  document.addEventListener("selectionchange", e => this.handleSelectionChangeEvent(e));
70
70
  this.e.addEventListener("paste", e => this.handlePaste(e));
71
- // this.e.addEventListener('keydown', (e) => this.handleKeyDown(e));
72
71
  this.lineElements = this.e.childNodes; // this will automatically update
73
72
  }
74
73
 
@@ -84,7 +83,7 @@ class Editor {
84
83
  this.lines = content.split(/(?:\r\n|\r|\n)/);
85
84
  this.lineDirty = [];
86
85
  for (let lineNum = 0; lineNum < this.lines.length; lineNum++) {
87
- let le = document.createElement('div');
86
+ let le = document.createElement("div");
88
87
  this.e.appendChild(le);
89
88
  this.lineDirty.push(true);
90
89
  }
@@ -98,7 +97,7 @@ class Editor {
98
97
  * @returns {string} The editor content as a markdown string
99
98
  */
100
99
  getContent() {
101
- return this.lines.join('\n');
100
+ return this.lines.join("\n");
102
101
  }
103
102
 
104
103
  /**
@@ -120,7 +119,7 @@ class Editor {
120
119
  updateLinkLabels() {
121
120
  this.linkLabels = [];
122
121
  for (let l = 0; l < this.lines.length; l++) {
123
- if (this.lineTypes[l] == 'TMLinkReferenceDefinition') {
122
+ if (this.lineTypes[l] == "TMLinkReferenceDefinition") {
124
123
  this.linkLabels.push(this.lineCaptures[l][_grammar.lineGrammar.TMLinkReferenceDefinition.labelPlaceholder]);
125
124
  }
126
125
  }
@@ -128,22 +127,22 @@ class Editor {
128
127
 
129
128
  /**
130
129
  * Helper function to replace placeholders from a RegExp capture. The replacement string can contain regular dollar placeholders (e.g., $1),
131
- * which are interpreted like in String.replace(), but also double dollar placeholders ($$1). In the case of double dollar placeholders,
130
+ * which are interpreted like in String.replace(), but also double dollar placeholders ($$1). In the case of double dollar placeholders,
132
131
  * Markdown inline grammar is applied on the content of the captured subgroup, i.e., $$1 processes inline Markdown grammar in the content of the
133
132
  * first captured subgroup, and replaces `$$1` with the result.
134
- *
133
+ *
135
134
  * @param {string} replacement The replacement string, including placeholders.
136
135
  * @param capture The result of a RegExp.exec() call
137
136
  * @returns The replacement string, with placeholders replaced from the capture result.
138
137
  */
139
138
  replace(replacement, capture) {
140
139
  return replacement.replace(/(\${1,2})([0-9])/g, (str, p1, p2) => {
141
- if (p1 == '$') return (0, _grammar.htmlescape)(capture[p2]);else return `<span class="TMInlineFormatted">${this.processInlineStyles(capture[p2])}</span>`;
140
+ if (p1 == "$") return (0, _grammar.htmlescape)(capture[p2]);else return `<span class="TMInlineFormatted">${this.processInlineStyles(capture[p2])}</span>`;
142
141
  });
143
142
  }
144
143
 
145
144
  /**
146
- * Applies the line types (from this.lineTypes as well as the capture result in this.lineReplacements and this.lineCaptures)
145
+ * Applies the line types (from this.lineTypes as well as the capture result in this.lineReplacements and this.lineCaptures)
147
146
  * and processes inline formatting for all lines.
148
147
  */
149
148
  applyLineTypes() {
@@ -152,8 +151,8 @@ class Editor {
152
151
  let contentHTML = this.replace(this.lineReplacements[lineNum], this.lineCaptures[lineNum]);
153
152
  // this.lineHTML[lineNum] = (contentHTML == '' ? '<br />' : contentHTML); // Prevent empty elements which can't be selected etc.
154
153
  this.lineElements[lineNum].className = this.lineTypes[lineNum];
155
- this.lineElements[lineNum].removeAttribute('style');
156
- this.lineElements[lineNum].innerHTML = contentHTML == '' ? '<br />' : contentHTML; // Prevent empty elements which can't be selected etc.
154
+ this.lineElements[lineNum].removeAttribute("style");
155
+ this.lineElements[lineNum].innerHTML = contentHTML == "" ? "<br />" : contentHTML; // Prevent empty elements which can't be selected etc.
157
156
  }
158
157
 
159
158
  this.lineElements[lineNum].dataset.lineNum = lineNum;
@@ -170,48 +169,48 @@ class Editor {
170
169
  let codeBlockSeqLength = 0;
171
170
  let htmlBlock = false;
172
171
  for (let lineNum = 0; lineNum < this.lines.length; lineNum++) {
173
- let lineType = 'TMPara';
172
+ let lineType = "TMPara";
174
173
  let lineCapture = [this.lines[lineNum]];
175
- let lineReplacement = '$$0'; // Default replacement for paragraph: Inline format the entire line
174
+ let lineReplacement = "$$0"; // Default replacement for paragraph: Inline format the entire line
176
175
 
177
176
  // Check ongoing code blocks
178
177
  // if (lineNum > 0 && (this.lineTypes[lineNum - 1] == 'TMCodeFenceBacktickOpen' || this.lineTypes[lineNum - 1] == 'TMFencedCodeBacktick')) {
179
- if (codeBlockType == 'TMCodeFenceBacktickOpen') {
178
+ if (codeBlockType == "TMCodeFenceBacktickOpen") {
180
179
  // We're in a backtick-fenced code block, check if the current line closes it
181
180
  let capture = _grammar.lineGrammar.TMCodeFenceBacktickClose.regexp.exec(this.lines[lineNum]);
182
- if (capture && capture.groups['seq'].length >= codeBlockSeqLength) {
183
- lineType = 'TMCodeFenceBacktickClose';
181
+ if (capture && capture.groups["seq"].length >= codeBlockSeqLength) {
182
+ lineType = "TMCodeFenceBacktickClose";
184
183
  lineReplacement = _grammar.lineGrammar.TMCodeFenceBacktickClose.replacement;
185
184
  lineCapture = capture;
186
185
  codeBlockType = false;
187
186
  } else {
188
- lineType = 'TMFencedCodeBacktick';
189
- lineReplacement = '$0';
187
+ lineType = "TMFencedCodeBacktick";
188
+ lineReplacement = "$0";
190
189
  lineCapture = [this.lines[lineNum]];
191
190
  }
192
191
  }
193
192
  // if (lineNum > 0 && (this.lineTypes[lineNum - 1] == 'TMCodeFenceTildeOpen' || this.lineTypes[lineNum - 1] == 'TMFencedCodeTilde')) {
194
- else if (codeBlockType == 'TMCodeFenceTildeOpen') {
193
+ else if (codeBlockType == "TMCodeFenceTildeOpen") {
195
194
  // We're in a tilde-fenced code block
196
195
  let capture = _grammar.lineGrammar.TMCodeFenceTildeClose.regexp.exec(this.lines[lineNum]);
197
- if (capture && capture.groups['seq'].length >= codeBlockSeqLength) {
198
- lineType = 'TMCodeFenceTildeClose';
196
+ if (capture && capture.groups["seq"].length >= codeBlockSeqLength) {
197
+ lineType = "TMCodeFenceTildeClose";
199
198
  lineReplacement = _grammar.lineGrammar.TMCodeFenceTildeClose.replacement;
200
199
  lineCapture = capture;
201
200
  codeBlockType = false;
202
201
  } else {
203
- lineType = 'TMFencedCodeTilde';
204
- lineReplacement = '$0';
202
+ lineType = "TMFencedCodeTilde";
203
+ lineReplacement = "$0";
205
204
  lineCapture = [this.lines[lineNum]];
206
205
  }
207
206
  }
208
207
 
209
208
  // Check HTML block types
210
- if (lineType == 'TMPara' && htmlBlock === false) {
209
+ if (lineType == "TMPara" && htmlBlock === false) {
211
210
  for (let htmlBlockType of _grammar.htmlBlockGrammar) {
212
211
  if (this.lines[lineNum].match(htmlBlockType.start)) {
213
212
  // Matching start condition. Check if this tag can start here (not all start conditions allow breaking a paragraph).
214
- if (htmlBlockType.paraInterrupt || lineNum == 0 || !(this.lineTypes[lineNum - 1] == 'TMPara' || this.lineTypes[lineNum - 1] == 'TMUL' || this.lineTypes[lineNum - 1] == 'TMOL' || this.lineTypes[lineNum - 1] == 'TMBlockquote')) {
213
+ if (htmlBlockType.paraInterrupt || lineNum == 0 || !(this.lineTypes[lineNum - 1] == "TMPara" || this.lineTypes[lineNum - 1] == "TMUL" || this.lineTypes[lineNum - 1] == "TMOL" || this.lineTypes[lineNum - 1] == "TMBlockquote")) {
215
214
  htmlBlock = htmlBlockType;
216
215
  break;
217
216
  }
@@ -219,8 +218,8 @@ class Editor {
219
218
  }
220
219
  }
221
220
  if (htmlBlock !== false) {
222
- lineType = 'TMHTMLBlock';
223
- lineReplacement = '$0'; // No formatting in TMHTMLBlock
221
+ lineType = "TMHTMLBlock";
222
+ lineReplacement = "$0"; // No formatting in TMHTMLBlock
224
223
  lineCapture = [this.lines[lineNum]]; // This should already be set but better safe than sorry
225
224
 
226
225
  // Check if HTML block should be closed
@@ -238,7 +237,7 @@ class Editor {
238
237
  }
239
238
 
240
239
  // Check all regexps if we haven't applied one of the code block types
241
- if (lineType == 'TMPara') {
240
+ if (lineType == "TMPara") {
242
241
  for (let type in _grammar.lineGrammar) {
243
242
  if (_grammar.lineGrammar[type].regexp) {
244
243
  let capture = _grammar.lineGrammar[type].regexp.exec(this.lines[lineNum]);
@@ -253,58 +252,58 @@ class Editor {
253
252
  }
254
253
 
255
254
  // If we've opened a code block, remember that
256
- if (lineType == 'TMCodeFenceBacktickOpen' || lineType == 'TMCodeFenceTildeOpen') {
255
+ if (lineType == "TMCodeFenceBacktickOpen" || lineType == "TMCodeFenceTildeOpen") {
257
256
  codeBlockType = lineType;
258
- codeBlockSeqLength = lineCapture.groups['seq'].length;
257
+ codeBlockSeqLength = lineCapture.groups["seq"].length;
259
258
  }
260
259
 
261
260
  // Link reference definition and indented code can't interrupt a paragraph
262
- if ((lineType == 'TMIndentedCode' || lineType == 'TMLinkReferenceDefinition') && lineNum > 0 && (this.lineTypes[lineNum - 1] == 'TMPara' || this.lineTypes[lineNum - 1] == 'TMUL' || this.lineTypes[lineNum - 1] == 'TMOL' || this.lineTypes[lineNum - 1] == 'TMBlockquote')) {
261
+ if ((lineType == "TMIndentedCode" || lineType == "TMLinkReferenceDefinition") && lineNum > 0 && (this.lineTypes[lineNum - 1] == "TMPara" || this.lineTypes[lineNum - 1] == "TMUL" || this.lineTypes[lineNum - 1] == "TMOL" || this.lineTypes[lineNum - 1] == "TMBlockquote")) {
263
262
  // Fall back to TMPara
264
- lineType = 'TMPara';
263
+ lineType = "TMPara";
265
264
  lineCapture = [this.lines[lineNum]];
266
- lineReplacement = '$$0';
265
+ lineReplacement = "$$0";
267
266
  }
268
267
 
269
268
  // Setext H2 markers that can also be interpreted as an empty list item should be regarded as such (as per CommonMark spec)
270
- if (lineType == 'TMSetextH2Marker') {
269
+ if (lineType == "TMSetextH2Marker") {
271
270
  let capture = _grammar.lineGrammar.TMUL.regexp.exec(this.lines[lineNum]);
272
271
  if (capture) {
273
- lineType = 'TMUL';
272
+ lineType = "TMUL";
274
273
  lineReplacement = _grammar.lineGrammar.TMUL.replacement;
275
274
  lineCapture = capture;
276
275
  }
277
276
  }
278
277
 
279
278
  // Setext headings are only valid if preceded by a paragraph (and if so, they change the type of the previous paragraph)
280
- if (lineType == 'TMSetextH1Marker' || lineType == 'TMSetextH2Marker') {
281
- if (lineNum == 0 || this.lineTypes[lineNum - 1] != 'TMPara') {
279
+ if (lineType == "TMSetextH1Marker" || lineType == "TMSetextH2Marker") {
280
+ if (lineNum == 0 || this.lineTypes[lineNum - 1] != "TMPara") {
282
281
  // Setext marker is invalid. However, a H2 marker might still be a valid HR, so let's check that
283
282
  let capture = _grammar.lineGrammar.TMHR.regexp.exec(this.lines[lineNum]);
284
283
  if (capture) {
285
284
  // Valid HR
286
- lineType = 'TMHR';
285
+ lineType = "TMHR";
287
286
  lineCapture = capture;
288
287
  lineReplacement = _grammar.lineGrammar.TMHR.replacement;
289
288
  } else {
290
289
  // Not valid HR, format as TMPara
291
- lineType = 'TMPara';
290
+ lineType = "TMPara";
292
291
  lineCapture = [this.lines[lineNum]];
293
- lineReplacement = '$$0';
292
+ lineReplacement = "$$0";
294
293
  }
295
294
  } else {
296
295
  // Valid setext marker. Change types of preceding para lines
297
296
  let headingLine = lineNum - 1;
298
- const headingLineType = lineType == 'TMSetextH1Marker' ? 'TMSetextH1' : 'TMSetextH2';
297
+ const headingLineType = lineType == "TMSetextH1Marker" ? "TMSetextH1" : "TMSetextH2";
299
298
  do {
300
299
  if (this.lineTypes[headingLineType] != headingLineType) {
301
300
  this.lineTypes[headingLine] = headingLineType;
302
301
  this.lineDirty[headingLineType] = true;
303
302
  }
304
- this.lineReplacements[headingLine] = '$$0';
303
+ this.lineReplacements[headingLine] = "$$0";
305
304
  this.lineCaptures[headingLine] = [this.lines[headingLine]];
306
305
  headingLine--;
307
- } while (headingLine >= 0 && this.lineTypes[headingLine] == 'TMPara');
306
+ } while (headingLine >= 0 && this.lineTypes[headingLine] == "TMPara");
308
307
  }
309
308
  }
310
309
  // Lastly, save the line style to be applied later
@@ -327,19 +326,19 @@ class Editor {
327
326
  }
328
327
 
329
328
  /**
330
- * Attempts to parse a link or image at the current position. This assumes that the opening [ or ![ has already been matched.
329
+ * Attempts to parse a link or image at the current position. This assumes that the opening [ or ![ has already been matched.
331
330
  * Returns false if this is not a valid link, image. See below for more information
332
331
  * @param {string} originalString The original string, starting at the opening marker ([ or ![)
333
332
  * @param {boolean} isImage Whether or not this is an image (opener == ![)
334
- * @returns false if not a valid link / image.
335
- * Otherwise returns an object with two properties: output is the string to be included in the processed output,
333
+ * @returns false if not a valid link / image.
334
+ * Otherwise returns an object with two properties: output is the string to be included in the processed output,
336
335
  * charCount is the number of input characters (from originalString) consumed.
337
336
  */
338
337
  parseLinkOrImage(originalString, isImage) {
339
338
  // Skip the opening bracket
340
339
  let textOffset = isImage ? 2 : 1;
341
340
  let opener = originalString.substr(0, textOffset);
342
- let type = isImage ? 'TMImage' : 'TMLink';
341
+ let type = isImage ? "TMImage" : "TMLink";
343
342
  let currentOffset = textOffset;
344
343
  let bracketLevel = 1;
345
344
  let linkText = false;
@@ -352,7 +351,7 @@ class Editor {
352
351
 
353
352
  // Capture any escapes and code blocks at current position, they bind more strongly than links
354
353
  // We don't have to actually process them here, that'll be done later in case the link / image is valid, but we need to skip over them.
355
- for (let rule of ['escape', 'code', 'autolink', 'html']) {
354
+ for (let rule of ["escape", "code", "autolink", "html"]) {
356
355
  let cap = _grammar.inlineGrammar[rule].regexp.exec(string);
357
356
  if (cap) {
358
357
  currentOffset += cap[0].length;
@@ -404,10 +403,10 @@ class Editor {
404
403
  if (linkText === false) return false; // Nope
405
404
 
406
405
  // So far, so good. We've got a valid link text. Let's see what type of link this is
407
- let nextChar = currentOffset < originalString.length ? originalString.substr(currentOffset, 1) : '';
406
+ let nextChar = currentOffset < originalString.length ? originalString.substr(currentOffset, 1) : "";
408
407
 
409
408
  // REFERENCE LINKS
410
- if (nextChar == '[') {
409
+ if (nextChar == "[") {
411
410
  let string = originalString.substr(currentOffset);
412
411
  let cap = _grammar.inlineGrammar.linkLabel.regexp.exec(string);
413
412
  if (cap) {
@@ -425,7 +424,7 @@ class Editor {
425
424
  // Not a valid link label
426
425
  return false;
427
426
  }
428
- } else if (nextChar != '(') {
427
+ } else if (nextChar != "(") {
429
428
  // Shortcut ref link
430
429
  linkRef = linkText.trim();
431
430
 
@@ -457,7 +456,7 @@ class Editor {
457
456
  linkDetails[1] = linkDetails[1].concat(cap[0]);
458
457
  } else {
459
458
  if (parenthesisLevel != 1) return false; // Unbalanced parenthesis
460
- linkDetails.push(''); // Empty end delimiter for destination
459
+ linkDetails.push(""); // Empty end delimiter for destination
461
460
  linkDetails.push(cap[0]); // Whitespace in between destination and title
462
461
  }
463
462
 
@@ -470,7 +469,7 @@ class Editor {
470
469
  return false;
471
470
  // This should never happen (no opener for title yet, but more whitespace to capture)
472
471
  case 5:
473
- linkDetails.push('');
472
+ linkDetails.push("");
474
473
  // Whitespace at beginning of title, push empty title and continue
475
474
  case 6:
476
475
  linkDetails[5] = linkDetails[5].concat(cap[0]);
@@ -494,7 +493,7 @@ class Editor {
494
493
  if (cap) {
495
494
  switch (linkDetails.length) {
496
495
  case 0:
497
- linkDetails.push('');
496
+ linkDetails.push("");
498
497
  // this opens the link destination, add empty opening delimiter and proceed to next case
499
498
  case 1:
500
499
  linkDetails.push(cap[0]);
@@ -511,7 +510,7 @@ class Editor {
511
510
  return false;
512
511
  // Lcaking opening delimiter for link title
513
512
  case 5:
514
- linkDetails.push('');
513
+ linkDetails.push("");
515
514
  // This opens the link title
516
515
  case 6:
517
516
  linkDetails[5] = linkDetails[5].concat(cap[0]);
@@ -528,26 +527,26 @@ class Editor {
528
527
 
529
528
  // Process opening angle bracket as deilimiter of destination
530
529
  if (linkDetails.length < 2 && string.match(/^</)) {
531
- if (linkDetails.length == 0) linkDetails.push('');
532
- linkDetails[0] = linkDetails[0].concat('<');
530
+ if (linkDetails.length == 0) linkDetails.push("");
531
+ linkDetails[0] = linkDetails[0].concat("<");
533
532
  currentOffset++;
534
533
  continue inlineOuter;
535
534
  }
536
535
 
537
536
  // Process closing angle bracket as delimiter of destination
538
537
  if ((linkDetails.length == 1 || linkDetails.length == 2) && string.match(/^>/)) {
539
- if (linkDetails.length == 1) linkDetails.push(''); // Empty link destination
540
- linkDetails.push('>');
538
+ if (linkDetails.length == 1) linkDetails.push(""); // Empty link destination
539
+ linkDetails.push(">");
541
540
  currentOffset++;
542
541
  continue inlineOuter;
543
542
  }
544
543
 
545
- // Process non-parenthesis delimiter for title.
544
+ // Process non-parenthesis delimiter for title.
546
545
  cap = /^["']/.exec(string);
547
546
  // For this to be a valid opener, we have to either have no destination, only whitespace so far,
548
547
  // or a destination with trailing whitespace.
549
548
  if (cap && (linkDetails.length == 0 || linkDetails.length == 1 || linkDetails.length == 4)) {
550
- while (linkDetails.length < 4) linkDetails.push('');
549
+ while (linkDetails.length < 4) linkDetails.push("");
551
550
  linkDetails.push(cap[0]);
552
551
  currentOffset++;
553
552
  continue inlineOuter;
@@ -555,7 +554,7 @@ class Editor {
555
554
 
556
555
  // For this to be a valid closer, we have to have an opener and some or no title, and this has to match the opener
557
556
  if (cap && (linkDetails.length == 5 || linkDetails.length == 6) && linkDetails[4] == cap[0]) {
558
- if (linkDetails.length == 5) linkDetails.push(''); // Empty link title
557
+ if (linkDetails.length == 5) linkDetails.push(""); // Empty link title
559
558
  linkDetails.push(cap[0]);
560
559
  currentOffset++;
561
560
  continue inlineOuter;
@@ -566,30 +565,30 @@ class Editor {
566
565
  if (string.match(/^\(/)) {
567
566
  switch (linkDetails.length) {
568
567
  case 0:
569
- linkDetails.push('');
568
+ linkDetails.push("");
570
569
  // this opens the link destination, add empty opening delimiter and proceed to next case
571
570
  case 1:
572
- linkDetails.push('');
571
+ linkDetails.push("");
573
572
  // This opens the link destination
574
573
  case 2:
575
574
  // Part of the link destination
576
- linkDetails[1] = linkDetails[1].concat('(');
575
+ linkDetails[1] = linkDetails[1].concat("(");
577
576
  if (!linkDetails[0].match(/<$/)) parenthesisLevel++;
578
577
  break;
579
578
  case 3:
580
- linkDetails.push('');
579
+ linkDetails.push("");
581
580
  // opening delimiter for link title
582
581
  case 4:
583
- linkDetails.push('(');
582
+ linkDetails.push("(");
584
583
  break;
585
584
  // opening delimiter for link title
586
585
  case 5:
587
- linkDetails.push('');
588
- // opens the link title, add empty title content and proceed to next case
586
+ linkDetails.push("");
587
+ // opens the link title, add empty title content and proceed to next case
589
588
  case 6:
590
589
  // Part of the link title. Un-escaped parenthesis only allowed in " or ' delimited title
591
- if (linkDetails[4] == '(') return false;
592
- linkDetails[5] = linkDetails[5].concat('(');
590
+ if (linkDetails[4] == "(") return false;
591
+ linkDetails[5] = linkDetails[5].concat("(");
593
592
  break;
594
593
  default:
595
594
  return false;
@@ -604,20 +603,20 @@ class Editor {
604
603
  if (string.match(/^\)/)) {
605
604
  if (linkDetails.length <= 2) {
606
605
  // We are inside the link destination. Parentheses have to be matched if not in angle brackets
607
- while (linkDetails.length < 2) linkDetails.push('');
606
+ while (linkDetails.length < 2) linkDetails.push("");
608
607
  if (!linkDetails[0].match(/<$/)) parenthesisLevel--;
609
608
  if (parenthesisLevel > 0) {
610
- linkDetails[1] = linkDetails[1].concat(')');
609
+ linkDetails[1] = linkDetails[1].concat(")");
611
610
  }
612
611
  } else if (linkDetails.length == 5 || linkDetails.length == 6) {
613
- // We are inside the link title.
614
- if (linkDetails[4] == '(') {
612
+ // We are inside the link title.
613
+ if (linkDetails[4] == "(") {
615
614
  // This closes the link title
616
- if (linkDetails.length == 5) linkDetails.push('');
617
- linkDetails.push(')');
615
+ if (linkDetails.length == 5) linkDetails.push("");
616
+ linkDetails.push(")");
618
617
  } else {
619
618
  // Just regular ol' content
620
- if (linkDetails.length == 5) linkDetails.push(')');else linkDetails[5] = linkDetails[5].concat(')');
619
+ if (linkDetails.length == 5) linkDetails.push(")");else linkDetails[5] = linkDetails[5].concat(")");
621
620
  }
622
621
  } else {
623
622
  parenthesisLevel--; // This should decrease it from 1 to 0...
@@ -625,7 +624,7 @@ class Editor {
625
624
 
626
625
  if (parenthesisLevel == 0) {
627
626
  // No invalid condition, let's make sure the linkDetails array is complete
628
- while (linkDetails.length < 7) linkDetails.push('');
627
+ while (linkDetails.length < 7) linkDetails.push("");
629
628
  }
630
629
  currentOffset++;
631
630
  continue inlineOuter;
@@ -636,7 +635,7 @@ class Editor {
636
635
  if (cap) {
637
636
  switch (linkDetails.length) {
638
637
  case 0:
639
- linkDetails.push('');
638
+ linkDetails.push("");
640
639
  // this opens the link destination, add empty opening delimiter and proceed to next case
641
640
  case 1:
642
641
  linkDetails.push(cap[0]);
@@ -653,7 +652,7 @@ class Editor {
653
652
  return false;
654
653
  // Lcaking opening delimiter for link title
655
654
  case 5:
656
- linkDetails.push('');
655
+ linkDetails.push("");
657
656
  // This opens the link title
658
657
  case 6:
659
658
  linkDetails[5] = linkDetails[5].concat(cap[0]);
@@ -696,7 +695,7 @@ class Editor {
696
695
 
697
696
  // This should never happen, but better safe than sorry.
698
697
  while (linkDetails.length < 7) {
699
- linkDetails.push('');
698
+ linkDetails.push("");
700
699
  }
701
700
  return {
702
701
  output: `<span class="TMMark TMMark_${type}">${opener}</span><span class="${type}">${this.processInlineStyles(linkText)}</span><span class="TMMark TMMark_${type}">](${linkDetails[0]}</span><span class="${type}Destination">${linkDetails[1]}</span><span class="TMMark TMMark_${type}">${linkDetails[2]}${linkDetails[3]}${linkDetails[4]}</span><span class="${type}Title">${linkDetails[5]}</span><span class="TMMark TMMark_${type}">${linkDetails[6]})</span>`,
@@ -712,13 +711,13 @@ class Editor {
712
711
  * @returns {string} The HTML formatted output
713
712
  */
714
713
  processInlineStyles(originalString) {
715
- let processed = '';
714
+ let processed = "";
716
715
  let stack = []; // Stack is an array of objects of the format: {delimiter, delimString, count, output}
717
716
  let offset = 0;
718
717
  let string = originalString;
719
718
  outer: while (string) {
720
719
  // Process simple rules (non-delimiter)
721
- for (let rule of ['escape', 'code', 'autolink', 'html']) {
720
+ for (let rule of ["escape", "code", "autolink", "html"]) {
722
721
  let cap = _grammar.inlineGrammar[rule].regexp.exec(string);
723
722
  if (cap) {
724
723
  string = string.substr(cap[0].length);
@@ -754,8 +753,8 @@ class Editor {
754
753
 
755
754
  // We have a delimiter run. Let's check if it can open or close an emphasis.
756
755
 
757
- const preceding = offset > 0 ? originalString.substr(0, offset) : ' '; // beginning and end of line count as whitespace
758
- const following = offset + cap[0].length < originalString.length ? string : ' ';
756
+ const preceding = offset > 0 ? originalString.substr(0, offset) : " "; // beginning and end of line count as whitespace
757
+ const following = offset + cap[0].length < originalString.length ? string : " ";
759
758
  const punctuationFollows = following.match(_grammar.punctuationLeading);
760
759
  const punctuationPrecedes = preceding.match(_grammar.punctuationTrailing);
761
760
  const whitespaceFollows = following.match(/^\s/);
@@ -766,7 +765,7 @@ class Editor {
766
765
  let canClose = !whitespacePrecedes && (!punctuationPrecedes || !!whitespaceFollows || !!punctuationFollows);
767
766
 
768
767
  // Underscores have more detailed rules than just being part of left- or right-flanking run:
769
- if (currentDelimiter == '_' && canOpen && canClose) {
768
+ if (currentDelimiter == "_" && canOpen && canClose) {
770
769
  canOpen = punctuationPrecedes;
771
770
  canClose = punctuationFollows;
772
771
  }
@@ -819,7 +818,7 @@ class Editor {
819
818
  count: delimCount,
820
819
  output: processed
821
820
  });
822
- processed = ''; // Current formatted output has been pushed on the stack and will be prepended when the stack gets popped
821
+ processed = ""; // Current formatted output has been pushed on the stack and will be prepended when the stack gets popped
823
822
  delimCount = 0;
824
823
  }
825
824
 
@@ -838,7 +837,7 @@ class Editor {
838
837
  let stackPointer = stack.length - 1;
839
838
  // See if we can find a matching opening delimiter, move down through the stack
840
839
  while (!consumed && stackPointer >= 0) {
841
- if (stack[stackPointer].delimiter == '~') {
840
+ if (stack[stackPointer].delimiter == "~") {
842
841
  // We found a matching delimiter, let's construct the formatted string
843
842
 
844
843
  // Firstly, if we skipped any stack levels, pop them immediately (non-matching delimiters)
@@ -862,12 +861,12 @@ class Editor {
862
861
  // If there are still delimiters left, and the delimiter run can open, push it on the stack
863
862
  if (!consumed) {
864
863
  stack.push({
865
- delimiter: '~',
866
- delimString: '~~',
864
+ delimiter: "~",
865
+ delimString: "~~",
867
866
  count: 2,
868
867
  output: processed
869
868
  });
870
- processed = ''; // Current formatted output has been pushed on the stack and will be prepended when the stack gets popped
869
+ processed = ""; // Current formatted output has been pushed on the stack and will be prepended when the stack gets popped
871
870
  }
872
871
 
873
872
  offset += cap[0].length;
@@ -883,7 +882,7 @@ class Editor {
883
882
  processed += _grammar.inlineGrammar.default.replacement.replace(/\$([1-9])/g, (str, p1) => (0, _grammar.htmlescape)(cap[p1]));
884
883
  continue outer;
885
884
  }
886
- throw 'Infinite loop!';
885
+ throw "Infinite loop!";
887
886
  }
888
887
 
889
888
  // Empty the stack, any opening delimiters are unused
@@ -894,7 +893,7 @@ class Editor {
894
893
  return processed;
895
894
  }
896
895
 
897
- /**
896
+ /**
898
897
  * Clears the line dirty flag (resets it to an array of false)
899
898
  */
900
899
  clearDirtyFlag() {
@@ -909,7 +908,7 @@ class Editor {
909
908
  * @returns true if contents changed
910
909
  */
911
910
  updateLineContents() {
912
- // this.lineDirty = [];
911
+ // this.lineDirty = [];
913
912
  // Check if we have changed anything about the number of lines (inserted or deleted a paragraph)
914
913
  // < 0 means line(s) removed; > 0 means line(s) added
915
914
  let lineDelta = this.e.childElementCount - this.lines.length;
@@ -917,8 +916,9 @@ class Editor {
917
916
  // yup. Let's try how much we can salvage (find out which lines from beginning and end were unchanged)
918
917
  // Find lines from the beginning that haven't changed...
919
918
  let firstChangedLine = 0;
920
- while (firstChangedLine <= this.lines.length && firstChangedLine <= this.lineElements.length && this.lineElements[firstChangedLine] // Check that the line element hasn't been deleted
921
- && this.lines[firstChangedLine] == this.lineElements[firstChangedLine].textContent) {
919
+ while (firstChangedLine <= this.lines.length && firstChangedLine <= this.lineElements.length && this.lineElements[firstChangedLine] &&
920
+ // Check that the line element hasn't been deleted
921
+ this.lines[firstChangedLine] == this.lineElements[firstChangedLine].textContent) {
922
922
  firstChangedLine++;
923
923
  }
924
924
 
@@ -963,17 +963,17 @@ class Editor {
963
963
 
964
964
  let checkLine = sel.col > 0 ? sel.row : sel.row - 1;
965
965
  switch (this.lineTypes[checkLine]) {
966
- case 'TMUL':
967
- continuableType = 'TMUL';
966
+ case "TMUL":
967
+ continuableType = "TMUL";
968
968
  break;
969
- case 'TMOL':
970
- continuableType = 'TMOL';
969
+ case "TMOL":
970
+ continuableType = "TMOL";
971
971
  break;
972
- case 'TMIndentedCode':
973
- continuableType = 'TMIndentedCode';
972
+ case "TMIndentedCode":
973
+ continuableType = "TMIndentedCode";
974
974
  break;
975
975
  }
976
- let lines = this.lines[sel.row].replace(/\n\n$/, '\n').split(/(?:\r\n|\n|\r)/);
976
+ let lines = this.lines[sel.row].replace(/\n\n$/, "\n").split(/(?:\r\n|\n|\r)/);
977
977
  if (lines.length == 1) {
978
978
  // No new line
979
979
  this.updateFormatting();
@@ -991,7 +991,7 @@ class Editor {
991
991
  // Previous line has content, continue the continuable type
992
992
 
993
993
  // Hack for OL: increment number
994
- if (continuableType == 'TMOL') {
994
+ if (continuableType == "TMOL") {
995
995
  capture[1] = capture[1].replace(/\d{1,9}/, result => {
996
996
  return parseInt(result[0]) + 1;
997
997
  });
@@ -1001,7 +1001,7 @@ class Editor {
1001
1001
  sel.col = capture[1].length;
1002
1002
  } else {
1003
1003
  // Previous line has no content, remove the continuable type from the previous row
1004
- this.lines[sel.row - 1] = '';
1004
+ this.lines[sel.row - 1] = "";
1005
1005
  this.lineDirty[sel.row - 1] = true;
1006
1006
  }
1007
1007
  }
@@ -1017,7 +1017,7 @@ class Editor {
1017
1017
  // processDelete(focus, forward) {
1018
1018
  // if (!focus) return;
1019
1019
  // let anchor = this.getSelection(true);
1020
- // // Do we have a non-empty selection?
1020
+ // // Do we have a non-empty selection?
1021
1021
  // if (focus.col != anchor.col || focus.row != anchor.row) {
1022
1022
  // // non-empty. direction doesn't matter.
1023
1023
  // this.paste('', anchor, focus);
@@ -1037,7 +1037,7 @@ class Editor {
1037
1037
 
1038
1038
  /**
1039
1039
  * Gets the current position of the selection counted by row and column of the editor Markdown content (as opposed to the position in the DOM).
1040
- *
1040
+ *
1041
1041
  * @param {boolean} getAnchor if set to true, gets the selection anchor (start point of the selection), otherwise gets the focus (end point).
1042
1042
  * @return {object} An object representing the selection, with properties col and row.
1043
1043
  */
@@ -1046,7 +1046,7 @@ class Editor {
1046
1046
  const selection = window.getSelection();
1047
1047
  let startNode = getAnchor ? selection.anchorNode : selection.focusNode;
1048
1048
  if (!startNode) return null;
1049
- let offset = startNode.nodeType === Node.TEXT_NODE ? getAnchor ? selection.anchorOffset : selection.focusOffset : 0;
1049
+ let offset = getAnchor ? selection.anchorOffset : selection.focusOffset;
1050
1050
  if (startNode == this.e) {
1051
1051
  return {
1052
1052
  row: 0,
@@ -1087,13 +1087,28 @@ class Editor {
1087
1087
  */
1088
1088
  computeColumn(startNode, offset) {
1089
1089
  let node = startNode;
1090
- let col = offset;
1090
+ let col;
1091
1091
  // First, make sure we're actually in the editor.
1092
1092
  while (node && node.parentNode != this.e) {
1093
1093
  node = node.parentNode;
1094
1094
  }
1095
1095
  if (node == null) return null;
1096
- node = startNode;
1096
+
1097
+ // There are two ways that offset can be defined:
1098
+ // - Either, the node is a text node, in which case it is the offset within the text
1099
+ // - Or, the node is an element with child notes, in which case the offset refers to the
1100
+ // child node after which the selection is located
1101
+ if (startNode.nodeType === Node.TEXT_NODE || offset === 0) {
1102
+ // In the case that the node is non-text node but the offset is 0,
1103
+ // The selection is at the beginning of that element so we
1104
+ // can simply use the same approach as if it were at the beginning
1105
+ // of a text node.
1106
+ col = offset;
1107
+ node = startNode;
1108
+ } else if (offset > 0) {
1109
+ node = startNode.childNodes[offset - 1];
1110
+ col = node.textContent.length;
1111
+ }
1097
1112
  while (node.parentNode != this.e) {
1098
1113
  if (node.previousSibling) {
1099
1114
  node = node.previousSibling;
@@ -1196,34 +1211,62 @@ class Editor {
1196
1211
  windowSelection.addRange(range);
1197
1212
  }
1198
1213
 
1199
- /**
1200
- * Event handler for input events
1214
+ /**
1215
+ * Event handler for input events
1201
1216
  */
1202
1217
  handleInputEvent(event) {
1218
+ // For composition input, we are only updating the text after we have received
1219
+ // a compositionend event, so we return upon insertCompositionText.
1220
+ // Otherwise, the DOM changes break the text input.
1221
+ if (event.inputType == "insertCompositionText") return;
1203
1222
  let focus = this.getSelection();
1204
- if ((event.inputType == 'insertParagraph' || event.inputType == 'insertLineBreak') && focus) {
1223
+ if ((event.inputType == "insertParagraph" || event.inputType == "insertLineBreak") && focus) {
1205
1224
  this.clearDirtyFlag();
1206
1225
  this.processNewParagraph(focus);
1207
1226
  } else {
1208
1227
  if (!this.e.firstChild) {
1209
1228
  this.e.innerHTML = '<div class="TMBlankLine"><br></div>';
1210
1229
  } else {
1211
- for (let childNode = this.e.firstChild; childNode; childNode = childNode.nextSibling) {
1212
- if (childNode.nodeType != 3 || childNode.tagName != 'DIV') {
1213
- // Found a child node that's either not an element or not a div. Wrap it in a div.
1214
- let divWrapper = document.createElement('div');
1215
- this.e.insertBefore(divWrapper, childNode);
1216
- this.e.removeChild(childNode);
1217
- divWrapper.appendChild(childNode);
1218
- }
1219
- }
1230
+ this.fixNodeHierarchy();
1220
1231
  }
1221
1232
  this.updateLineContentsAndFormatting();
1222
1233
  }
1223
- if (focus) this.setSelection(focus);
1234
+ if (focus) {
1235
+ this.setSelection(focus);
1236
+ }
1224
1237
  this.fireChange();
1225
1238
  }
1226
1239
 
1240
+ /**
1241
+ * Fixes the node hierarchy – makes sure that each line is in a div, and there are no nested divs
1242
+ */
1243
+ fixNodeHierarchy() {
1244
+ const originalChildren = Array.from(this.e.childNodes);
1245
+ const replaceChild = function (child) {
1246
+ const parent = child.parentElement;
1247
+ const nextSibling = child.nextSibling;
1248
+ parent.removeChild(child);
1249
+ for (var _len = arguments.length, newChildren = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
1250
+ newChildren[_key - 1] = arguments[_key];
1251
+ }
1252
+ newChildren.forEach(newChild => nextSibling ? parent.insertBefore(nextSibling, newChild) : parent.appendChild(newChild));
1253
+ };
1254
+ originalChildren.forEach(child => {
1255
+ // child.parentElement.removeChild(child);
1256
+ if (child.nodeType !== Node.ELEMENT_NODE || child.tagName !== "DIV") {
1257
+ // Found a child node that's either not an element or not a div. Wrap it in a div.
1258
+ const divWrapper = document.createElement("div");
1259
+ replaceChild(child, divWrapper);
1260
+ divWrapper.appendChild(child);
1261
+ } else {
1262
+ const grandChildren = Array.from(child.childNodes);
1263
+ if (grandChildren.some(grandChild => grandChild.nodeType === Node.ELEMENT_NODE && grandChild.tagName === "DIV")) {
1264
+ return replaceChild(child, grandChildren);
1265
+ }
1266
+ }
1267
+ });
1268
+ }
1269
+
1227
1270
  /**
1228
1271
  * Event handler for "selectionchange" events.
1229
1272
  */
@@ -1232,11 +1275,11 @@ class Editor {
1232
1275
  }
1233
1276
 
1234
1277
  /**
1235
- * Convenience function to "splice" new lines into the arrays this.lines, this.lineDirty, this.lineTypes, and the DOM elements
1278
+ * Convenience function to "splice" new lines into the arrays this.lines, this.lineDirty, this.lineTypes, and the DOM elements
1236
1279
  * underneath the editor element.
1237
1280
  * This method is essentially Array.splice, only that the third parameter takes an un-spread array (and the forth parameter)
1238
1281
  * determines whether the DOM should also be adjusted.
1239
- *
1282
+ *
1240
1283
  * @param {int} startLine Position at which to start changing the array of lines
1241
1284
  * @param {int} linesToDelete Number of lines to delete
1242
1285
  * @param {array} linesToInsert Array of strings representing the lines to be inserted
@@ -1254,10 +1297,10 @@ class Editor {
1254
1297
  let insertedBlank = [];
1255
1298
  let insertedDirty = [];
1256
1299
  for (let i = 0; i < linesToInsert.length; i++) {
1257
- insertedBlank.push('');
1300
+ insertedBlank.push("");
1258
1301
  insertedDirty.push(true);
1259
1302
  if (adjustLineElements) {
1260
- if (this.e.childNodes[startLine]) this.e.insertBefore(document.createElement('div'), this.e.childNodes[startLine]);else this.e.appendChild(document.createElement('div'));
1303
+ if (this.e.childNodes[startLine]) this.e.insertBefore(document.createElement("div"), this.e.childNodes[startLine]);else this.e.appendChild(document.createElement("div"));
1261
1304
  }
1262
1305
  }
1263
1306
  this.lines.splice(startLine, linesToDelete, ...linesToInsert);
@@ -1272,7 +1315,7 @@ class Editor {
1272
1315
  event.preventDefault();
1273
1316
 
1274
1317
  // get text representation of clipboard
1275
- let text = (event.originalEvent || event).clipboardData.getData('text/plain');
1318
+ let text = (event.originalEvent || event).clipboardData.getData("text/plain");
1276
1319
 
1277
1320
  // insert text manually
1278
1321
  this.paste(text);
@@ -1280,7 +1323,7 @@ class Editor {
1280
1323
 
1281
1324
  /**
1282
1325
  * Pastes the text at the current selection (or at the end, if no current selection)
1283
- * @param {string} text
1326
+ * @param {string} text
1284
1327
  */
1285
1328
  paste(text) {
1286
1329
  let anchor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
@@ -1401,18 +1444,17 @@ class Editor {
1401
1444
  }
1402
1445
 
1403
1446
  for (let cmd in _grammar.commands) {
1404
- if (_grammar.commands[cmd].type == 'inline') {
1447
+ if (_grammar.commands[cmd].type == "inline") {
1405
1448
  if (!focus || focus.row != anchor.row || !this.isInlineFormattingAllowed(focus, anchor)) {
1406
1449
  commandState[cmd] = null;
1407
1450
  } else {
1408
- // The command state is true if there is a respective enclosing markup node (e.g., the selection is enclosed in a <b>..</b>) ...
1451
+ // The command state is true if there is a respective enclosing markup node (e.g., the selection is enclosed in a <b>..</b>) ...
1409
1452
  commandState[cmd] = !!this.computeEnclosingMarkupNode(focus, anchor, _grammar.commands[cmd].className) ||
1410
1453
  // ... or if it's an empty string preceded by and followed by formatting markers, e.g. **|** where | is the cursor
1411
-
1412
1454
  focus.col == anchor.col && !!this.lines[focus.row].substr(0, focus.col).match(_grammar.commands[cmd].unset.prePattern) && !!this.lines[focus.row].substr(focus.col).match(_grammar.commands[cmd].unset.postPattern);
1413
1455
  }
1414
1456
  }
1415
- if (_grammar.commands[cmd].type == 'line') {
1457
+ if (_grammar.commands[cmd].type == "line") {
1416
1458
  if (!focus) {
1417
1459
  commandState[cmd] = null;
1418
1460
  } else {
@@ -1432,11 +1474,11 @@ class Editor {
1432
1474
 
1433
1475
  /**
1434
1476
  * Sets a command state
1435
- * @param {string} command
1436
- * @param {boolean} state
1477
+ * @param {string} command
1478
+ * @param {boolean} state
1437
1479
  */
1438
1480
  setCommandState(command, state) {
1439
- if (_grammar.commands[command].type == 'inline') {
1481
+ if (_grammar.commands[command].type == "inline") {
1440
1482
  let anchor = this.getSelection(true);
1441
1483
  let focus = this.getSelection(false);
1442
1484
  if (!anchor) anchor = focus;
@@ -1451,9 +1493,9 @@ class Editor {
1451
1493
  this.lineDirty[focus.row] = true;
1452
1494
  const startCol = this.computeColumn(markupNode, 0);
1453
1495
  const len = markupNode.textContent.length;
1454
- const left = this.lines[focus.row].substr(0, startCol).replace(_grammar.commands[command].unset.prePattern, '');
1496
+ const left = this.lines[focus.row].substr(0, startCol).replace(_grammar.commands[command].unset.prePattern, "");
1455
1497
  const mid = this.lines[focus.row].substr(startCol, len);
1456
- const right = this.lines[focus.row].substr(startCol + len).replace(_grammar.commands[command].unset.postPattern, '');
1498
+ const right = this.lines[focus.row].substr(startCol + len).replace(_grammar.commands[command].unset.postPattern, "");
1457
1499
  this.lines[focus.row] = left.concat(mid, right);
1458
1500
  anchor.col = left.length;
1459
1501
  focus.col = anchor.col + len;
@@ -1464,8 +1506,8 @@ class Editor {
1464
1506
  // Second case: Empty selection with surrounding formatting markers, remove those
1465
1507
  } else if (focus.col == anchor.col && !!this.lines[focus.row].substr(0, focus.col).match(_grammar.commands[command].unset.prePattern) && !!this.lines[focus.row].substr(focus.col).match(_grammar.commands[command].unset.postPattern)) {
1466
1508
  this.lineDirty[focus.row] = true;
1467
- const left = this.lines[focus.row].substr(0, focus.col).replace(_grammar.commands[command].unset.prePattern, '');
1468
- const right = this.lines[focus.row].substr(focus.col).replace(_grammar.commands[command].unset.postPattern, '');
1509
+ const left = this.lines[focus.row].substr(0, focus.col).replace(_grammar.commands[command].unset.prePattern, "");
1510
+ const right = this.lines[focus.row].substr(focus.col).replace(_grammar.commands[command].unset.postPattern, "");
1469
1511
  this.lines[focus.row] = left.concat(right);
1470
1512
  focus.col = anchor.col = left.length;
1471
1513
  this.updateFormatting();
@@ -1493,12 +1535,12 @@ class Editor {
1493
1535
  focus.col = startCol;
1494
1536
  anchor.col = endCol;
1495
1537
 
1496
- // Just insert markup before and after and hope for the best.
1538
+ // Just insert markup before and after and hope for the best.
1497
1539
  this.wrapSelection(_grammar.commands[command].set.pre, _grammar.commands[command].set.post, focus, anchor);
1498
1540
  this.fireChange();
1499
1541
  // TODO clean this up so that markup remains properly nested
1500
1542
  }
1501
- } else if (_grammar.commands[command].type == 'line') {
1543
+ } else if (_grammar.commands[command].type == "line") {
1502
1544
  let anchor = this.getSelection(true);
1503
1545
  let focus = this.getSelection(false);
1504
1546
  if (!anchor) anchor = focus;
@@ -1511,7 +1553,7 @@ class Editor {
1511
1553
  }
1512
1554
  for (let line = start.row; line <= end.row; line++) {
1513
1555
  if (state && this.lineTypes[line] != _grammar.commands[command].className) {
1514
- this.lines[line] = this.lines[line].replace(_grammar.commands[command].set.pattern, _grammar.commands[command].set.replacement.replace('$#', line - start.row + 1));
1556
+ this.lines[line] = this.lines[line].replace(_grammar.commands[command].set.pattern, _grammar.commands[command].set.replacement.replace("$#", line - start.row + 1));
1515
1557
  this.lineDirty[line] = true;
1516
1558
  }
1517
1559
  if (!state && this.lineTypes[line] == _grammar.commands[command].className) {
@@ -1532,7 +1574,7 @@ class Editor {
1532
1574
  }
1533
1575
 
1534
1576
  /**
1535
- * Returns whether or not inline formatting is allowed at the current focus
1577
+ * Returns whether or not inline formatting is allowed at the current focus
1536
1578
  * @param {object} focus The current focus
1537
1579
  */
1538
1580
  isInlineFormattingAllowed() {
@@ -1540,13 +1582,13 @@ class Editor {
1540
1582
  const sel = window.getSelection();
1541
1583
  if (!sel || !sel.focusNode || !sel.anchorNode) return false;
1542
1584
 
1543
- // Check if we can find a common ancestor with the class `TMInlineFormatted`
1585
+ // Check if we can find a common ancestor with the class `TMInlineFormatted`
1544
1586
 
1545
1587
  // Special case: Empty selection right before `TMInlineFormatted`
1546
1588
  if (sel.isCollapsed && sel.focusNode.nodeType == 3 && sel.focusOffset == sel.focusNode.nodeValue.length) {
1547
1589
  let node;
1548
1590
  for (node = sel.focusNode; node && node.nextSibling == null; node = node.parentNode);
1549
- if (node && node.nextSibling.className && node.nextSibling.className.includes('TMInlineFormatted')) return true;
1591
+ if (node && node.nextSibling.className && node.nextSibling.className.includes("TMInlineFormatted")) return true;
1550
1592
  }
1551
1593
 
1552
1594
  // Look for a common ancestor
@@ -1555,7 +1597,7 @@ class Editor {
1555
1597
 
1556
1598
  // Check if there's an ancestor of class 'TMInlineFormatted' or 'TMBlankLine'
1557
1599
  while (ancestor && ancestor != this.e) {
1558
- if (ancestor.className && (ancestor.className.includes('TMInlineFormatted') || ancestor.className.includes('TMBlankLine'))) return true;
1600
+ if (ancestor.className && (ancestor.className.includes("TMInlineFormatted") || ancestor.className.includes("TMBlankLine"))) return true;
1559
1601
  ancestor = ancestor.parentNode;
1560
1602
  }
1561
1603
  return false;
@@ -1578,7 +1620,7 @@ class Editor {
1578
1620
  const startCol = focus.col < anchor.col ? focus.col : anchor.col;
1579
1621
  const endCol = focus.col < anchor.col ? anchor.col : focus.col;
1580
1622
  const left = this.lines[focus.row].substr(0, startCol).concat(pre);
1581
- const mid = endCol == startCol ? '' : this.lines[focus.row].substr(startCol, endCol - startCol);
1623
+ const mid = endCol == startCol ? "" : this.lines[focus.row].substr(startCol, endCol - startCol);
1582
1624
  const right = post.concat(this.lines[focus.row].substr(endCol));
1583
1625
  this.lines[focus.row] = left.concat(mid, right);
1584
1626
  anchor.col = left.length;