tiny-markdown-editor 0.1.4 → 0.1.6

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
@@ -1,122 +1,13 @@
1
1
  "use strict";
2
2
 
3
- require("core-js/modules/es.symbol");
4
-
5
- require("core-js/modules/es.symbol.description");
6
-
7
- require("core-js/modules/es.symbol.iterator");
8
-
9
- require("core-js/modules/es.symbol.replace");
10
-
11
- require("core-js/modules/es.array.concat");
12
-
13
- require("core-js/modules/es.array.from");
14
-
15
- require("core-js/modules/es.array.includes");
16
-
17
- require("core-js/modules/es.array.index-of");
18
-
19
- require("core-js/modules/es.array.iterator");
20
-
21
- require("core-js/modules/es.array.join");
22
-
23
- require("core-js/modules/es.array.reduce");
24
-
25
- require("core-js/modules/es.array.slice");
26
-
27
- require("core-js/modules/es.array.splice");
28
-
29
- require("core-js/modules/es.function.name");
30
-
31
- require("core-js/modules/es.map");
32
-
33
- require("core-js/modules/es.object.assign");
34
-
35
- require("core-js/modules/es.object.get-prototype-of");
36
-
37
- require("core-js/modules/es.object.keys");
38
-
39
- require("core-js/modules/es.object.to-string");
40
-
41
- require("core-js/modules/es.reflect.construct");
42
-
43
- require("core-js/modules/es.regexp.constructor");
44
-
45
- require("core-js/modules/es.regexp.exec");
46
-
47
- require("core-js/modules/es.regexp.to-string");
48
-
49
- require("core-js/modules/es.string.includes");
50
-
51
- require("core-js/modules/es.string.iterator");
52
-
53
- require("core-js/modules/es.string.match");
54
-
55
- require("core-js/modules/es.string.replace");
56
-
57
- require("core-js/modules/es.string.split");
58
-
59
- require("core-js/modules/es.string.trim");
60
-
61
- require("core-js/modules/es.weak-map");
62
-
63
- require("core-js/modules/web.dom-collections.iterator");
64
-
65
3
  Object.defineProperty(exports, "__esModule", {
66
4
  value: true
67
5
  });
68
6
  exports.default = void 0;
69
-
70
7
  var _grammar = require("./grammar");
71
-
72
- function _wrapRegExp(re, groups) { _wrapRegExp = function _wrapRegExp(re, groups) { return new BabelRegExp(re, undefined, groups); }; var _RegExp = _wrapNativeSuper(RegExp); var _super = RegExp.prototype; var _groups = new WeakMap(); function BabelRegExp(re, flags, groups) { var _this = _RegExp.call(this, re, flags); _groups.set(_this, groups || _groups.get(re)); return _this; } _inherits(BabelRegExp, _RegExp); BabelRegExp.prototype.exec = function (str) { var result = _super.exec.call(this, str); if (result) result.groups = buildGroups(result, this); return result; }; BabelRegExp.prototype[Symbol.replace] = function (str, substitution) { if (typeof substitution === "string") { var groups = _groups.get(this); return _super[Symbol.replace].call(this, str, substitution.replace(/\$<([^>]+)>/g, function (_, name) { return "$" + groups[name]; })); } else if (typeof substitution === "function") { var _this = this; return _super[Symbol.replace].call(this, str, function () { var args = []; args.push.apply(args, arguments); if (_typeof(args[args.length - 1]) !== "object") { args.push(buildGroups(args, _this)); } return substitution.apply(this, args); }); } else { return _super[Symbol.replace].call(this, str, substitution); } }; function buildGroups(result, re) { var g = _groups.get(re); return Object.keys(g).reduce(function (groups, name) { groups[name] = result[g[name]]; return groups; }, Object.create(null)); } return _wrapRegExp.apply(this, arguments); }
73
-
74
- function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
75
-
76
- function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
77
-
78
- function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
79
-
80
- function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); }
81
-
82
- function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); }
83
-
84
- function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
85
-
86
- function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; }
87
-
88
- function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
89
-
90
- function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
91
-
92
- function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
93
-
94
- function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
95
-
96
- function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); }
97
-
98
- function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
99
-
100
- function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
101
-
102
- function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
103
-
104
- function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
105
-
106
- function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
107
-
108
- function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
109
-
110
- function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
111
-
112
- function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
113
-
114
- var Editor = /*#__PURE__*/function () {
115
- function Editor() {
116
- var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
117
-
118
- _classCallCheck(this, Editor);
119
-
8
+ class Editor {
9
+ constructor() {
10
+ let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
120
11
  this.e = null;
121
12
  this.textarea = null;
122
13
  this.lines = [];
@@ -131,1942 +22,1624 @@ var Editor = /*#__PURE__*/function () {
131
22
  change: [],
132
23
  selection: []
133
24
  };
134
- var element = props.element;
25
+ let element = props.element;
135
26
  this.textarea = props.textarea;
136
-
137
27
  if (this.textarea && !this.textarea.tagName) {
138
28
  this.textarea = document.getElementById(this.textarea);
139
29
  if (!element) element = this.textarea;
140
30
  }
141
-
142
31
  if (element && !element.tagName) {
143
32
  element = document.getElementById(props.element);
144
33
  }
145
-
146
34
  if (!element) {
147
35
  element = document.getElementsByTagName('body')[0];
148
36
  }
149
-
150
37
  if (element.tagName == 'TEXTAREA') {
151
38
  this.textarea = element;
152
39
  element = this.textarea.parentNode;
153
40
  }
154
-
155
41
  if (this.textarea) {
156
42
  this.textarea.style.display = 'none';
157
43
  }
158
-
159
- this.createEditorElement(element); // TODO Placeholder for empty content
160
-
44
+ this.createEditorElement(element);
45
+ // TODO Placeholder for empty content
161
46
  this.setContent(props.content || (this.textarea ? this.textarea.value : false) || '# Hello TinyMDE!\nEdit **here**');
162
47
  }
48
+
163
49
  /**
164
50
  * Creates the editor element inside the target element of the DOM tree
165
51
  * @param element The target element of the DOM tree
166
52
  */
53
+ createEditorElement(element) {
54
+ this.e = document.createElement('div');
55
+ this.e.className = 'TinyMDE';
56
+ this.e.contentEditable = true;
57
+ // The following is important for formatting purposes, but also since otherwise the browser replaces subsequent spaces with &nbsp; &nbsp;
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';
60
+ // Avoid formatting (B / I / U) popping up on iOS
61
+ this.e.style.webkitUserModify = 'read-write-plaintext-only';
62
+ if (this.textarea && this.textarea.parentNode == element && this.textarea.nextSibling) {
63
+ element.insertBefore(this.e, this.textarea.nextSibling);
64
+ } else {
65
+ element.appendChild(this.e);
66
+ }
67
+ this.e.addEventListener("input", e => this.handleInputEvent(e));
68
+ // this.e.addEventListener("keydown", (e) => this.handleKeydownEvent(e));
69
+ document.addEventListener("selectionchange", e => this.handleSelectionChangeEvent(e));
70
+ this.e.addEventListener("paste", e => this.handlePaste(e));
71
+ // this.e.addEventListener('keydown', (e) => this.handleKeyDown(e));
72
+ this.lineElements = this.e.childNodes; // this will automatically update
73
+ }
167
74
 
75
+ /**
76
+ * Sets the editor content.
77
+ * @param {string} content The new Markdown content
78
+ */
79
+ setContent(content) {
80
+ // Delete any existing content
81
+ while (this.e.firstChild) {
82
+ this.e.removeChild(this.e.firstChild);
83
+ }
84
+ this.lines = content.split(/(?:\r\n|\r|\n)/);
85
+ this.lineDirty = [];
86
+ for (let lineNum = 0; lineNum < this.lines.length; lineNum++) {
87
+ let le = document.createElement('div');
88
+ this.e.appendChild(le);
89
+ this.lineDirty.push(true);
90
+ }
91
+ this.lineTypes = new Array(this.lines.length);
92
+ this.updateFormatting();
93
+ this.fireChange();
94
+ }
168
95
 
169
- _createClass(Editor, [{
170
- key: "createEditorElement",
171
- value: function createEditorElement(element) {
172
- var _this = this;
173
-
174
- this.e = document.createElement('div');
175
- this.e.className = 'TinyMDE';
176
- this.e.contentEditable = true; // The following is important for formatting purposes, but also since otherwise the browser replaces subsequent spaces with &nbsp; &nbsp;
177
- // 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
178
-
179
- this.e.style.whiteSpace = 'pre-wrap'; // Avoid formatting (B / I / U) popping up on iOS
96
+ /**
97
+ * Gets the editor content as a Markdown string.
98
+ * @returns {string} The editor content as a markdown string
99
+ */
100
+ getContent() {
101
+ return this.lines.join('\n');
102
+ }
180
103
 
181
- this.e.style.webkitUserModify = 'read-write-plaintext-only';
104
+ /**
105
+ * This is the main method to update the formatting (from this.lines to HTML output)
106
+ */
107
+ updateFormatting() {
108
+ // First, parse line types. This will update this.lineTypes, this.lineReplacements, and this.lineCaptures
109
+ // We don't apply the formatting yet
110
+ this.updateLineTypes();
111
+ // Collect any valid link labels from link reference definitions—we need that for formatting to determine what's a valid link
112
+ this.updateLinkLabels();
113
+ // Now, apply the formatting
114
+ this.applyLineTypes();
115
+ }
182
116
 
183
- if (this.textarea && this.textarea.parentNode == element && this.textarea.nextSibling) {
184
- element.insertBefore(this.e, this.textarea.nextSibling);
185
- } else {
186
- element.appendChild(this.e);
117
+ /**
118
+ * Updates this.linkLabels: For every link reference definition (line type TMLinkReferenceDefinition), we collect the label
119
+ */
120
+ updateLinkLabels() {
121
+ this.linkLabels = [];
122
+ for (let l = 0; l < this.lines.length; l++) {
123
+ if (this.lineTypes[l] == 'TMLinkReferenceDefinition') {
124
+ this.linkLabels.push(this.lineCaptures[l][_grammar.lineGrammar.TMLinkReferenceDefinition.labelPlaceholder]);
187
125
  }
126
+ }
127
+ }
188
128
 
189
- this.e.addEventListener("input", function (e) {
190
- return _this.handleInputEvent(e);
191
- }); // this.e.addEventListener("keydown", (e) => this.handleKeydownEvent(e));
129
+ /**
130
+ * 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,
132
+ * 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
+ * first captured subgroup, and replaces `$$1` with the result.
134
+ *
135
+ * @param {string} replacement The replacement string, including placeholders.
136
+ * @param capture The result of a RegExp.exec() call
137
+ * @returns The replacement string, with placeholders replaced from the capture result.
138
+ */
139
+ replace(replacement, capture) {
140
+ 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>`;
142
+ });
143
+ }
192
144
 
193
- document.addEventListener("selectionchange", function (e) {
194
- return _this.handleSelectionChangeEvent(e);
195
- });
196
- this.e.addEventListener("paste", function (e) {
197
- return _this.handlePaste(e);
198
- }); // this.e.addEventListener('keydown', (e) => this.handleKeyDown(e));
145
+ /**
146
+ * Applies the line types (from this.lineTypes as well as the capture result in this.lineReplacements and this.lineCaptures)
147
+ * and processes inline formatting for all lines.
148
+ */
149
+ applyLineTypes() {
150
+ for (let lineNum = 0; lineNum < this.lines.length; lineNum++) {
151
+ if (this.lineDirty[lineNum]) {
152
+ let contentHTML = this.replace(this.lineReplacements[lineNum], this.lineCaptures[lineNum]);
153
+ // this.lineHTML[lineNum] = (contentHTML == '' ? '<br />' : contentHTML); // Prevent empty elements which can't be selected etc.
154
+ 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.
157
+ }
199
158
 
200
- this.lineElements = this.e.childNodes; // this will automatically update
159
+ this.lineElements[lineNum].dataset.lineNum = lineNum;
201
160
  }
202
- /**
203
- * Sets the editor content.
204
- * @param {string} content The new Markdown content
205
- */
206
-
207
- }, {
208
- key: "setContent",
209
- value: function setContent(content) {
210
- // Delete any existing content
211
- while (this.e.firstChild) {
212
- this.e.removeChild(this.e.firstChild);
213
- }
161
+ }
214
162
 
215
- this.lines = content.split(/(?:\r\n|\r|\n)/);
216
- this.lineDirty = [];
163
+ /**
164
+ * Determines line types for all lines based on the line / block grammar. Captures the results of the respective line
165
+ * grammar regular expressions.
166
+ * Updates this.lineTypes, this.lineCaptures, and this.lineReplacements.
167
+ */
168
+ updateLineTypes() {
169
+ let codeBlockType = false;
170
+ let codeBlockSeqLength = 0;
171
+ let htmlBlock = false;
172
+ for (let lineNum = 0; lineNum < this.lines.length; lineNum++) {
173
+ let lineType = 'TMPara';
174
+ let lineCapture = [this.lines[lineNum]];
175
+ let lineReplacement = '$$0'; // Default replacement for paragraph: Inline format the entire line
176
+
177
+ // Check ongoing code blocks
178
+ // if (lineNum > 0 && (this.lineTypes[lineNum - 1] == 'TMCodeFenceBacktickOpen' || this.lineTypes[lineNum - 1] == 'TMFencedCodeBacktick')) {
179
+ if (codeBlockType == 'TMCodeFenceBacktickOpen') {
180
+ // We're in a backtick-fenced code block, check if the current line closes it
181
+ let capture = _grammar.lineGrammar.TMCodeFenceBacktickClose.regexp.exec(this.lines[lineNum]);
182
+ if (capture && capture.groups['seq'].length >= codeBlockSeqLength) {
183
+ lineType = 'TMCodeFenceBacktickClose';
184
+ lineReplacement = _grammar.lineGrammar.TMCodeFenceBacktickClose.replacement;
185
+ lineCapture = capture;
186
+ codeBlockType = false;
187
+ } else {
188
+ lineType = 'TMFencedCodeBacktick';
189
+ lineReplacement = '$0';
190
+ lineCapture = [this.lines[lineNum]];
191
+ }
192
+ }
193
+ // if (lineNum > 0 && (this.lineTypes[lineNum - 1] == 'TMCodeFenceTildeOpen' || this.lineTypes[lineNum - 1] == 'TMFencedCodeTilde')) {
194
+ else if (codeBlockType == 'TMCodeFenceTildeOpen') {
195
+ // We're in a tilde-fenced code block
196
+ let capture = _grammar.lineGrammar.TMCodeFenceTildeClose.regexp.exec(this.lines[lineNum]);
197
+ if (capture && capture.groups['seq'].length >= codeBlockSeqLength) {
198
+ lineType = 'TMCodeFenceTildeClose';
199
+ lineReplacement = _grammar.lineGrammar.TMCodeFenceTildeClose.replacement;
200
+ lineCapture = capture;
201
+ codeBlockType = false;
202
+ } else {
203
+ lineType = 'TMFencedCodeTilde';
204
+ lineReplacement = '$0';
205
+ lineCapture = [this.lines[lineNum]];
206
+ }
207
+ }
217
208
 
218
- for (var lineNum = 0; lineNum < this.lines.length; lineNum++) {
219
- var le = document.createElement('div');
220
- this.e.appendChild(le);
221
- this.lineDirty.push(true);
209
+ // Check HTML block types
210
+ if (lineType == 'TMPara' && htmlBlock === false) {
211
+ for (let htmlBlockType of _grammar.htmlBlockGrammar) {
212
+ if (this.lines[lineNum].match(htmlBlockType.start)) {
213
+ // 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')) {
215
+ htmlBlock = htmlBlockType;
216
+ break;
217
+ }
218
+ }
219
+ }
220
+ }
221
+ if (htmlBlock !== false) {
222
+ lineType = 'TMHTMLBlock';
223
+ lineReplacement = '$0'; // No formatting in TMHTMLBlock
224
+ lineCapture = [this.lines[lineNum]]; // This should already be set but better safe than sorry
225
+
226
+ // Check if HTML block should be closed
227
+ if (htmlBlock.end) {
228
+ // Specific end condition
229
+ if (this.lines[lineNum].match(htmlBlock.end)) {
230
+ htmlBlock = false;
231
+ }
232
+ } else {
233
+ // No specific end condition, ends with blank line
234
+ if (lineNum == this.lines.length - 1 || this.lines[lineNum + 1].match(_grammar.lineGrammar.TMBlankLine.regexp)) {
235
+ htmlBlock = false;
236
+ }
237
+ }
222
238
  }
223
239
 
224
- this.lineTypes = new Array(this.lines.length);
225
- this.updateFormatting();
226
- this.fireChange();
227
- }
228
- /**
229
- * Gets the editor content as a Markdown string.
230
- * @returns {string} The editor content as a markdown string
231
- */
232
-
233
- }, {
234
- key: "getContent",
235
- value: function getContent() {
236
- return this.lines.join('\n');
237
- }
238
- /**
239
- * This is the main method to update the formatting (from this.lines to HTML output)
240
- */
240
+ // Check all regexps if we haven't applied one of the code block types
241
+ if (lineType == 'TMPara') {
242
+ for (let type in _grammar.lineGrammar) {
243
+ if (_grammar.lineGrammar[type].regexp) {
244
+ let capture = _grammar.lineGrammar[type].regexp.exec(this.lines[lineNum]);
245
+ if (capture) {
246
+ lineType = type;
247
+ lineReplacement = _grammar.lineGrammar[type].replacement;
248
+ lineCapture = capture;
249
+ break;
250
+ }
251
+ }
252
+ }
253
+ }
241
254
 
242
- }, {
243
- key: "updateFormatting",
244
- value: function updateFormatting() {
245
- // First, parse line types. This will update this.lineTypes, this.lineReplacements, and this.lineCaptures
246
- // We don't apply the formatting yet
247
- this.updateLineTypes(); // Collect any valid link labels from link reference definitions—we need that for formatting to determine what's a valid link
255
+ // If we've opened a code block, remember that
256
+ if (lineType == 'TMCodeFenceBacktickOpen' || lineType == 'TMCodeFenceTildeOpen') {
257
+ codeBlockType = lineType;
258
+ codeBlockSeqLength = lineCapture.groups['seq'].length;
259
+ }
248
260
 
249
- this.updateLinkLabels(); // Now, apply the formatting
261
+ // 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')) {
263
+ // Fall back to TMPara
264
+ lineType = 'TMPara';
265
+ lineCapture = [this.lines[lineNum]];
266
+ lineReplacement = '$$0';
267
+ }
250
268
 
251
- this.applyLineTypes();
252
- }
253
- /**
254
- * Updates this.linkLabels: For every link reference definition (line type TMLinkReferenceDefinition), we collect the label
255
- */
256
-
257
- }, {
258
- key: "updateLinkLabels",
259
- value: function updateLinkLabels() {
260
- this.linkLabels = [];
261
-
262
- for (var l = 0; l < this.lines.length; l++) {
263
- if (this.lineTypes[l] == 'TMLinkReferenceDefinition') {
264
- this.linkLabels.push(this.lineCaptures[l][_grammar.lineGrammar.TMLinkReferenceDefinition.labelPlaceholder]);
269
+ // 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') {
271
+ let capture = _grammar.lineGrammar.TMUL.regexp.exec(this.lines[lineNum]);
272
+ if (capture) {
273
+ lineType = 'TMUL';
274
+ lineReplacement = _grammar.lineGrammar.TMUL.replacement;
275
+ lineCapture = capture;
265
276
  }
266
277
  }
267
- }
268
- /**
269
- * Helper function to replace placeholders from a RegExp capture. The replacement string can contain regular dollar placeholders (e.g., $1),
270
- * which are interpreted like in String.replace(), but also double dollar placeholders ($$1). In the case of double dollar placeholders,
271
- * Markdown inline grammar is applied on the content of the captured subgroup, i.e., $$1 processes inline Markdown grammar in the content of the
272
- * first captured subgroup, and replaces `$$1` with the result.
273
- *
274
- * @param {string} replacement The replacement string, including placeholders.
275
- * @param capture The result of a RegExp.exec() call
276
- * @returns The replacement string, with placeholders replaced from the capture result.
277
- */
278
-
279
- }, {
280
- key: "replace",
281
- value: function replace(replacement, capture) {
282
- var _this2 = this;
283
-
284
- return replacement.replace(/\$\$([0-9])/g, function (str, p1) {
285
- return "<span class=\"TMInlineFormatted\">".concat(_this2.processInlineStyles(capture[p1]), "</span>");
286
- }).replace(/\$([0-9])/g, function (str, p1) {
287
- return (0, _grammar.htmlescape)(capture[p1]);
288
- });
289
- }
290
- /**
291
- * Applies the line types (from this.lineTypes as well as the capture result in this.lineReplacements and this.lineCaptures)
292
- * and processes inline formatting for all lines.
293
- */
294
-
295
- }, {
296
- key: "applyLineTypes",
297
- value: function applyLineTypes() {
298
- for (var lineNum = 0; lineNum < this.lines.length; lineNum++) {
299
- if (this.lineDirty[lineNum]) {
300
- var contentHTML = this.replace(this.lineReplacements[lineNum], this.lineCaptures[lineNum]); // this.lineHTML[lineNum] = (contentHTML == '' ? '<br />' : contentHTML); // Prevent empty elements which can't be selected etc.
301
-
302
- this.lineElements[lineNum].className = this.lineTypes[lineNum];
303
- this.lineElements[lineNum].removeAttribute('style');
304
- this.lineElements[lineNum].innerHTML = contentHTML == '' ? '<br />' : contentHTML; // Prevent empty elements which can't be selected etc.
305
- }
306
278
 
307
- this.lineElements[lineNum].dataset.lineNum = lineNum;
308
- }
309
- }
310
- /**
311
- * Determines line types for all lines based on the line / block grammar. Captures the results of the respective line
312
- * grammar regular expressions.
313
- * Updates this.lineTypes, this.lineCaptures, and this.lineReplacements.
314
- */
315
-
316
- }, {
317
- key: "updateLineTypes",
318
- value: function updateLineTypes() {
319
- var codeBlockType = false;
320
- var codeBlockSeqLength = 0;
321
- var htmlBlock = false;
322
-
323
- for (var lineNum = 0; lineNum < this.lines.length; lineNum++) {
324
- var lineType = 'TMPara';
325
- var lineCapture = [this.lines[lineNum]];
326
- var lineReplacement = '$$0'; // Default replacement for paragraph: Inline format the entire line
327
- // Check ongoing code blocks
328
- // if (lineNum > 0 && (this.lineTypes[lineNum - 1] == 'TMCodeFenceBacktickOpen' || this.lineTypes[lineNum - 1] == 'TMFencedCodeBacktick')) {
329
-
330
- if (codeBlockType == 'TMCodeFenceBacktickOpen') {
331
- // We're in a backtick-fenced code block, check if the current line closes it
332
- var capture = _grammar.lineGrammar.TMCodeFenceBacktickClose.regexp.exec(this.lines[lineNum]);
333
-
334
- if (capture && capture.groups['seq'].length >= codeBlockSeqLength) {
335
- lineType = 'TMCodeFenceBacktickClose';
336
- lineReplacement = _grammar.lineGrammar.TMCodeFenceBacktickClose.replacement;
279
+ // 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') {
282
+ // Setext marker is invalid. However, a H2 marker might still be a valid HR, so let's check that
283
+ let capture = _grammar.lineGrammar.TMHR.regexp.exec(this.lines[lineNum]);
284
+ if (capture) {
285
+ // Valid HR
286
+ lineType = 'TMHR';
337
287
  lineCapture = capture;
338
- codeBlockType = false;
288
+ lineReplacement = _grammar.lineGrammar.TMHR.replacement;
339
289
  } else {
340
- lineType = 'TMFencedCodeBacktick';
341
- lineReplacement = '$0';
290
+ // Not valid HR, format as TMPara
291
+ lineType = 'TMPara';
342
292
  lineCapture = [this.lines[lineNum]];
293
+ lineReplacement = '$$0';
343
294
  }
344
- } // if (lineNum > 0 && (this.lineTypes[lineNum - 1] == 'TMCodeFenceTildeOpen' || this.lineTypes[lineNum - 1] == 'TMFencedCodeTilde')) {
345
- else if (codeBlockType == 'TMCodeFenceTildeOpen') {
346
- // We're in a tilde-fenced code block
347
- var _capture = _grammar.lineGrammar.TMCodeFenceTildeClose.regexp.exec(this.lines[lineNum]);
348
-
349
- if (_capture && _capture.groups['seq'].length >= codeBlockSeqLength) {
350
- lineType = 'TMCodeFenceTildeClose';
351
- lineReplacement = _grammar.lineGrammar.TMCodeFenceTildeClose.replacement;
352
- lineCapture = _capture;
353
- codeBlockType = false;
354
- } else {
355
- lineType = 'TMFencedCodeTilde';
356
- lineReplacement = '$0';
357
- lineCapture = [this.lines[lineNum]];
295
+ } else {
296
+ // Valid setext marker. Change types of preceding para lines
297
+ let headingLine = lineNum - 1;
298
+ const headingLineType = lineType == 'TMSetextH1Marker' ? 'TMSetextH1' : 'TMSetextH2';
299
+ do {
300
+ if (this.lineTypes[headingLineType] != headingLineType) {
301
+ this.lineTypes[headingLine] = headingLineType;
302
+ this.lineDirty[headingLineType] = true;
358
303
  }
359
- } // Check HTML block types
304
+ this.lineReplacements[headingLine] = '$$0';
305
+ this.lineCaptures[headingLine] = [this.lines[headingLine]];
306
+ headingLine--;
307
+ } while (headingLine >= 0 && this.lineTypes[headingLine] == 'TMPara');
308
+ }
309
+ }
310
+ // Lastly, save the line style to be applied later
311
+ if (this.lineTypes[lineNum] != lineType) {
312
+ this.lineTypes[lineNum] = lineType;
313
+ this.lineDirty[lineNum] = true;
314
+ }
315
+ this.lineReplacements[lineNum] = lineReplacement;
316
+ this.lineCaptures[lineNum] = lineCapture;
317
+ }
318
+ }
360
319
 
320
+ /**
321
+ * Updates all line contents from the HTML, then re-applies formatting.
322
+ */
323
+ updateLineContentsAndFormatting() {
324
+ this.clearDirtyFlag();
325
+ this.updateLineContents();
326
+ this.updateFormatting();
327
+ }
361
328
 
362
- if (lineType == 'TMPara' && htmlBlock === false) {
363
- var _iterator = _createForOfIteratorHelper(_grammar.htmlBlockGrammar),
364
- _step;
329
+ /**
330
+ * Attempts to parse a link or image at the current position. This assumes that the opening [ or ![ has already been matched.
331
+ * Returns false if this is not a valid link, image. See below for more information
332
+ * @param {string} originalString The original string, starting at the opening marker ([ or ![)
333
+ * @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,
336
+ * charCount is the number of input characters (from originalString) consumed.
337
+ */
338
+ parseLinkOrImage(originalString, isImage) {
339
+ // Skip the opening bracket
340
+ let textOffset = isImage ? 2 : 1;
341
+ let opener = originalString.substr(0, textOffset);
342
+ let type = isImage ? 'TMImage' : 'TMLink';
343
+ let currentOffset = textOffset;
344
+ let bracketLevel = 1;
345
+ let linkText = false;
346
+ let linkRef = false;
347
+ let linkLabel = [];
348
+ let linkDetails = []; // If matched, this will be an array: [whitespace + link destination delimiter, link destination, link destination delimiter, whitespace, link title delimiter, link title, link title delimiter + whitespace]. All can be empty strings.
349
+
350
+ textOuter: while (currentOffset < originalString.length && linkText === false /* empty string is okay */) {
351
+ let string = originalString.substr(currentOffset);
352
+
353
+ // Capture any escapes and code blocks at current position, they bind more strongly than links
354
+ // 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']) {
356
+ let cap = _grammar.inlineGrammar[rule].regexp.exec(string);
357
+ if (cap) {
358
+ currentOffset += cap[0].length;
359
+ continue textOuter;
360
+ }
361
+ }
365
362
 
366
- try {
367
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
368
- var htmlBlockType = _step.value;
363
+ // Check for image. It's okay for an image to be included in a link or image
364
+ if (string.match(_grammar.inlineGrammar.imageOpen.regexp)) {
365
+ // Opening image. It's okay if this is a matching pair of brackets
366
+ bracketLevel++;
367
+ currentOffset += 2;
368
+ continue textOuter;
369
+ }
369
370
 
370
- if (this.lines[lineNum].match(htmlBlockType.start)) {
371
- // Matching start condition. Check if this tag can start here (not all start conditions allow breaking a paragraph).
372
- 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')) {
373
- htmlBlock = htmlBlockType;
374
- break;
375
- }
376
- }
377
- }
378
- } catch (err) {
379
- _iterator.e(err);
380
- } finally {
381
- _iterator.f();
371
+ // Check for link (not an image because that would have been captured and skipped over above)
372
+ if (string.match(_grammar.inlineGrammar.linkOpen.regexp)) {
373
+ // Opening bracket. Two things to do:
374
+ // 1) it's okay if this part of a pair of brackets.
375
+ // 2) If we are currently trying to parse a link, this nested bracket musn't start a valid link (no nested links allowed)
376
+ bracketLevel++;
377
+ // if (bracketLevel >= 2) return false; // Nested unescaped brackets, this doesn't qualify as a link / image
378
+ if (!isImage) {
379
+ if (this.parseLinkOrImage(string, false)) {
380
+ // Valid link inside this possible link, which makes this link invalid (inner links beat outer ones)
381
+ return false;
382
382
  }
383
383
  }
384
+ currentOffset += 1;
385
+ continue textOuter;
386
+ }
384
387
 
385
- if (htmlBlock !== false) {
386
- lineType = 'TMHTMLBlock';
387
- lineReplacement = '$0'; // No formatting in TMHTMLBlock
388
+ // Check for closing bracket
389
+ if (string.match(/^\]/)) {
390
+ bracketLevel--;
391
+ if (bracketLevel == 0) {
392
+ // Found matching bracket and haven't found anything disqualifying this as link / image.
393
+ linkText = originalString.substr(textOffset, currentOffset - textOffset);
394
+ currentOffset++;
395
+ continue textOuter;
396
+ }
397
+ }
388
398
 
389
- lineCapture = [this.lines[lineNum]]; // This should already be set but better safe than sorry
390
- // Check if HTML block should be closed
399
+ // Nothing matches, proceed to next char
400
+ currentOffset++;
401
+ }
391
402
 
392
- if (htmlBlock.end) {
393
- // Specific end condition
394
- if (this.lines[lineNum].match(htmlBlock.end)) {
395
- htmlBlock = false;
396
- }
397
- } else {
398
- // No specific end condition, ends with blank line
399
- if (lineNum == this.lines.length - 1 || this.lines[lineNum + 1].match(_grammar.lineGrammar.TMBlankLine.regexp)) {
400
- htmlBlock = false;
401
- }
402
- }
403
- } // Check all regexps if we haven't applied one of the code block types
403
+ // Did we find a link text (i.e., find a matching closing bracket?)
404
+ if (linkText === false) return false; // Nope
405
+
406
+ // 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) : '';
408
+
409
+ // REFERENCE LINKS
410
+ if (nextChar == '[') {
411
+ let string = originalString.substr(currentOffset);
412
+ let cap = _grammar.inlineGrammar.linkLabel.regexp.exec(string);
413
+ if (cap) {
414
+ // Valid link label
415
+ currentOffset += cap[0].length;
416
+ linkLabel.push(cap[1], cap[2], cap[3]);
417
+ if (cap[_grammar.inlineGrammar.linkLabel.labelPlaceholder]) {
418
+ // Full reference link
419
+ linkRef = cap[_grammar.inlineGrammar.linkLabel.labelPlaceholder];
420
+ } else {
421
+ // Collapsed reference link
422
+ linkRef = linkText.trim();
423
+ }
424
+ } else {
425
+ // Not a valid link label
426
+ return false;
427
+ }
428
+ } else if (nextChar != '(') {
429
+ // Shortcut ref link
430
+ linkRef = linkText.trim();
431
+
432
+ // INLINE LINKS
433
+ } else {
434
+ // nextChar == '('
435
+
436
+ // Potential inline link
437
+ currentOffset++;
438
+ let parenthesisLevel = 1;
439
+ inlineOuter: while (currentOffset < originalString.length && parenthesisLevel > 0) {
440
+ let string = originalString.substr(currentOffset);
441
+
442
+ // Process whitespace
443
+ let cap = /^\s+/.exec(string);
444
+ if (cap) {
445
+ switch (linkDetails.length) {
446
+ case 0:
447
+ linkDetails.push(cap[0]);
448
+ break;
449
+ // Opening whitespace
450
+ case 1:
451
+ linkDetails.push(cap[0]);
452
+ break;
453
+ // Open destination, but not a destination yet; desination opened with <
454
+ case 2:
455
+ // Open destination with content in it. Whitespace only allowed if opened by angle bracket, otherwise this closes the destination
456
+ if (linkDetails[0].match(/</)) {
457
+ linkDetails[1] = linkDetails[1].concat(cap[0]);
458
+ } else {
459
+ if (parenthesisLevel != 1) return false; // Unbalanced parenthesis
460
+ linkDetails.push(''); // Empty end delimiter for destination
461
+ linkDetails.push(cap[0]); // Whitespace in between destination and title
462
+ }
404
463
 
464
+ break;
465
+ case 3:
466
+ linkDetails.push(cap[0]);
467
+ break;
468
+ // Whitespace between destination and title
469
+ case 4:
470
+ return false;
471
+ // This should never happen (no opener for title yet, but more whitespace to capture)
472
+ case 5:
473
+ linkDetails.push('');
474
+ // Whitespace at beginning of title, push empty title and continue
475
+ case 6:
476
+ linkDetails[5] = linkDetails[5].concat(cap[0]);
477
+ break;
478
+ // Whitespace in title
479
+ case 7:
480
+ linkDetails[6] = linkDetails[6].concat(cap[0]);
481
+ break;
482
+ // Whitespace after closing delimiter
483
+ default:
484
+ return false;
485
+ // We should never get here
486
+ }
405
487
 
406
- if (lineType == 'TMPara') {
407
- for (var type in _grammar.lineGrammar) {
408
- if (_grammar.lineGrammar[type].regexp) {
409
- var _capture2 = _grammar.lineGrammar[type].regexp.exec(this.lines[lineNum]);
488
+ currentOffset += cap[0].length;
489
+ continue inlineOuter;
490
+ }
410
491
 
411
- if (_capture2) {
412
- lineType = type;
413
- lineReplacement = _grammar.lineGrammar[type].replacement;
414
- lineCapture = _capture2;
415
- break;
416
- }
417
- }
492
+ // Process backslash escapes
493
+ cap = _grammar.inlineGrammar.escape.regexp.exec(string);
494
+ if (cap) {
495
+ switch (linkDetails.length) {
496
+ case 0:
497
+ linkDetails.push('');
498
+ // this opens the link destination, add empty opening delimiter and proceed to next case
499
+ case 1:
500
+ linkDetails.push(cap[0]);
501
+ break;
502
+ // This opens the link destination, append it
503
+ case 2:
504
+ linkDetails[1] = linkDetails[1].concat(cap[0]);
505
+ break;
506
+ // Part of the link destination
507
+ case 3:
508
+ return false;
509
+ // Lacking opening delimiter for link title
510
+ case 4:
511
+ return false;
512
+ // Lcaking opening delimiter for link title
513
+ case 5:
514
+ linkDetails.push('');
515
+ // This opens the link title
516
+ case 6:
517
+ linkDetails[5] = linkDetails[5].concat(cap[0]);
518
+ break;
519
+ // Part of the link title
520
+ default:
521
+ return false;
522
+ // After link title was closed, without closing parenthesis
418
523
  }
419
- } // If we've opened a code block, remember that
420
-
421
524
 
422
- if (lineType == 'TMCodeFenceBacktickOpen' || lineType == 'TMCodeFenceTildeOpen') {
423
- codeBlockType = lineType;
424
- codeBlockSeqLength = lineCapture.groups['seq'].length;
425
- } // Link reference definition and indented code can't interrupt a paragraph
525
+ currentOffset += cap[0].length;
526
+ continue inlineOuter;
527
+ }
426
528
 
529
+ // Process opening angle bracket as deilimiter of destination
530
+ if (linkDetails.length < 2 && string.match(/^</)) {
531
+ if (linkDetails.length == 0) linkDetails.push('');
532
+ linkDetails[0] = linkDetails[0].concat('<');
533
+ currentOffset++;
534
+ continue inlineOuter;
535
+ }
427
536
 
428
- 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')) {
429
- // Fall back to TMPara
430
- lineType = 'TMPara';
431
- lineCapture = [this.lines[lineNum]];
432
- lineReplacement = '$$0';
433
- } // Setext H2 markers that can also be interpreted as an empty list item should be regarded as such (as per CommonMark spec)
537
+ // Process closing angle bracket as delimiter of destination
538
+ if ((linkDetails.length == 1 || linkDetails.length == 2) && string.match(/^>/)) {
539
+ if (linkDetails.length == 1) linkDetails.push(''); // Empty link destination
540
+ linkDetails.push('>');
541
+ currentOffset++;
542
+ continue inlineOuter;
543
+ }
434
544
 
545
+ // Process non-parenthesis delimiter for title.
546
+ cap = /^["']/.exec(string);
547
+ // For this to be a valid opener, we have to either have no destination, only whitespace so far,
548
+ // or a destination with trailing whitespace.
549
+ if (cap && (linkDetails.length == 0 || linkDetails.length == 1 || linkDetails.length == 4)) {
550
+ while (linkDetails.length < 4) linkDetails.push('');
551
+ linkDetails.push(cap[0]);
552
+ currentOffset++;
553
+ continue inlineOuter;
554
+ }
435
555
 
436
- if (lineType == 'TMSetextH2Marker') {
437
- var _capture3 = _grammar.lineGrammar.TMUL.regexp.exec(this.lines[lineNum]);
556
+ // 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
+ if (cap && (linkDetails.length == 5 || linkDetails.length == 6) && linkDetails[4] == cap[0]) {
558
+ if (linkDetails.length == 5) linkDetails.push(''); // Empty link title
559
+ linkDetails.push(cap[0]);
560
+ currentOffset++;
561
+ continue inlineOuter;
562
+ }
563
+ // Other cases (linkDetails.length == 2, 3, 7) will be handled with the "default" case below.
438
564
 
439
- if (_capture3) {
440
- lineType = 'TMUL';
441
- lineReplacement = _grammar.lineGrammar.TMUL.replacement;
442
- lineCapture = _capture3;
565
+ // Process opening parenthesis
566
+ if (string.match(/^\(/)) {
567
+ switch (linkDetails.length) {
568
+ case 0:
569
+ linkDetails.push('');
570
+ // this opens the link destination, add empty opening delimiter and proceed to next case
571
+ case 1:
572
+ linkDetails.push('');
573
+ // This opens the link destination
574
+ case 2:
575
+ // Part of the link destination
576
+ linkDetails[1] = linkDetails[1].concat('(');
577
+ if (!linkDetails[0].match(/<$/)) parenthesisLevel++;
578
+ break;
579
+ case 3:
580
+ linkDetails.push('');
581
+ // opening delimiter for link title
582
+ case 4:
583
+ linkDetails.push('(');
584
+ break;
585
+ // opening delimiter for link title
586
+ case 5:
587
+ linkDetails.push('');
588
+ // opens the link title, add empty title content and proceed to next case
589
+ case 6:
590
+ // 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('(');
593
+ break;
594
+ default:
595
+ return false;
596
+ // After link title was closed, without closing parenthesis
443
597
  }
444
- } // Setext headings are only valid if preceded by a paragraph (and if so, they change the type of the previous paragraph)
445
-
446
598
 
447
- if (lineType == 'TMSetextH1Marker' || lineType == 'TMSetextH2Marker') {
448
- if (lineNum == 0 || this.lineTypes[lineNum - 1] != 'TMPara') {
449
- // Setext marker is invalid. However, a H2 marker might still be a valid HR, so let's check that
450
- var _capture4 = _grammar.lineGrammar.TMHR.regexp.exec(this.lines[lineNum]);
599
+ currentOffset++;
600
+ continue inlineOuter;
601
+ }
451
602
 
452
- if (_capture4) {
453
- // Valid HR
454
- lineType = 'TMHR';
455
- lineCapture = _capture4;
456
- lineReplacement = _grammar.lineGrammar.TMHR.replacement;
603
+ // Process closing parenthesis
604
+ if (string.match(/^\)/)) {
605
+ if (linkDetails.length <= 2) {
606
+ // We are inside the link destination. Parentheses have to be matched if not in angle brackets
607
+ while (linkDetails.length < 2) linkDetails.push('');
608
+ if (!linkDetails[0].match(/<$/)) parenthesisLevel--;
609
+ if (parenthesisLevel > 0) {
610
+ linkDetails[1] = linkDetails[1].concat(')');
611
+ }
612
+ } else if (linkDetails.length == 5 || linkDetails.length == 6) {
613
+ // We are inside the link title.
614
+ if (linkDetails[4] == '(') {
615
+ // This closes the link title
616
+ if (linkDetails.length == 5) linkDetails.push('');
617
+ linkDetails.push(')');
457
618
  } else {
458
- // Not valid HR, format as TMPara
459
- lineType = 'TMPara';
460
- lineCapture = [this.lines[lineNum]];
461
- lineReplacement = '$$0';
619
+ // Just regular ol' content
620
+ if (linkDetails.length == 5) linkDetails.push(')');else linkDetails[5] = linkDetails[5].concat(')');
462
621
  }
463
622
  } else {
464
- // Valid setext marker. Change types of preceding para lines
465
- var headingLine = lineNum - 1;
466
- var headingLineType = lineType == 'TMSetextH1Marker' ? 'TMSetextH1' : 'TMSetextH2';
467
-
468
- do {
469
- if (this.lineTypes[headingLineType] != headingLineType) {
470
- this.lineTypes[headingLine] = headingLineType;
471
- this.lineDirty[headingLineType] = true;
472
- }
623
+ parenthesisLevel--; // This should decrease it from 1 to 0...
624
+ }
473
625
 
474
- this.lineReplacements[headingLine] = '$$0';
475
- this.lineCaptures[headingLine] = [this.lines[headingLine]];
476
- headingLine--;
477
- } while (headingLine >= 0 && this.lineTypes[headingLine] == 'TMPara');
626
+ if (parenthesisLevel == 0) {
627
+ // No invalid condition, let's make sure the linkDetails array is complete
628
+ while (linkDetails.length < 7) linkDetails.push('');
478
629
  }
479
- } // Lastly, save the line style to be applied later
630
+ currentOffset++;
631
+ continue inlineOuter;
632
+ }
480
633
 
634
+ // Any old character
635
+ cap = /^./.exec(string);
636
+ if (cap) {
637
+ switch (linkDetails.length) {
638
+ case 0:
639
+ linkDetails.push('');
640
+ // this opens the link destination, add empty opening delimiter and proceed to next case
641
+ case 1:
642
+ linkDetails.push(cap[0]);
643
+ break;
644
+ // This opens the link destination, append it
645
+ case 2:
646
+ linkDetails[1] = linkDetails[1].concat(cap[0]);
647
+ break;
648
+ // Part of the link destination
649
+ case 3:
650
+ return false;
651
+ // Lacking opening delimiter for link title
652
+ case 4:
653
+ return false;
654
+ // Lcaking opening delimiter for link title
655
+ case 5:
656
+ linkDetails.push('');
657
+ // This opens the link title
658
+ case 6:
659
+ linkDetails[5] = linkDetails[5].concat(cap[0]);
660
+ break;
661
+ // Part of the link title
662
+ default:
663
+ return false;
664
+ // After link title was closed, without closing parenthesis
665
+ }
481
666
 
482
- if (this.lineTypes[lineNum] != lineType) {
483
- this.lineTypes[lineNum] = lineType;
484
- this.lineDirty[lineNum] = true;
667
+ currentOffset += cap[0].length;
668
+ continue inlineOuter;
485
669
  }
486
-
487
- this.lineReplacements[lineNum] = lineReplacement;
488
- this.lineCaptures[lineNum] = lineCapture;
670
+ throw "Infinite loop"; // we should never get here since the last test matches any character
489
671
  }
490
- }
491
- /**
492
- * Updates all line contents from the HTML, then re-applies formatting.
493
- */
494
672
 
495
- }, {
496
- key: "updateLineContentsAndFormatting",
497
- value: function updateLineContentsAndFormatting() {
498
- this.clearDirtyFlag();
499
- this.updateLineContents();
500
- this.updateFormatting();
673
+ if (parenthesisLevel > 0) return false; // Parenthes(es) not closed
501
674
  }
502
- /**
503
- * Attempts to parse a link or image at the current position. This assumes that the opening [ or ![ has already been matched.
504
- * Returns false if this is not a valid link, image. See below for more information
505
- * @param {string} originalString The original string, starting at the opening marker ([ or ![)
506
- * @param {boolean} isImage Whether or not this is an image (opener == ![)
507
- * @returns false if not a valid link / image.
508
- * Otherwise returns an object with two properties: output is the string to be included in the processed output,
509
- * charCount is the number of input characters (from originalString) consumed.
510
- */
511
-
512
- }, {
513
- key: "parseLinkOrImage",
514
- value: function parseLinkOrImage(originalString, isImage) {
515
- // Skip the opening bracket
516
- var textOffset = isImage ? 2 : 1;
517
- var opener = originalString.substr(0, textOffset);
518
- var type = isImage ? 'TMImage' : 'TMLink';
519
- var currentOffset = textOffset;
520
- var bracketLevel = 1;
521
- var linkText = false;
522
- var linkRef = false;
523
- var linkLabel = [];
524
- var linkDetails = []; // If matched, this will be an array: [whitespace + link destination delimiter, link destination, link destination delimiter, whitespace, link title delimiter, link title, link title delimiter + whitespace]. All can be empty strings.
525
-
526
- textOuter: while (currentOffset < originalString.length && linkText === false
527
- /* empty string is okay */
528
- ) {
529
- var string = originalString.substr(currentOffset); // Capture any escapes and code blocks at current position, they bind more strongly than links
530
- // 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.
531
-
532
- for (var _i = 0, _arr = ['escape', 'code', 'autolink', 'html']; _i < _arr.length; _i++) {
533
- var rule = _arr[_i];
534
-
535
- var cap = _grammar.inlineGrammar[rule].regexp.exec(string);
536
-
537
- if (cap) {
538
- currentOffset += cap[0].length;
539
- continue textOuter;
540
- }
541
- } // Check for image. It's okay for an image to be included in a link or image
542
675
 
676
+ if (linkRef !== false) {
677
+ // Ref link; check that linkRef is valid
678
+ let valid = false;
679
+ for (let label of this.linkLabels) {
680
+ if (label == linkRef) {
681
+ valid = true;
682
+ break;
683
+ }
684
+ }
685
+ let label = valid ? "TMLinkLabel TMLinkLabel_Valid" : "TMLinkLabel TMLinkLabel_Invalid";
686
+ let output = `<span class="TMMark TMMark_${type}">${opener}</span><span class="${type} ${linkLabel.length < 3 || !linkLabel[1] ? label : ""}">${this.processInlineStyles(linkText)}</span><span class="TMMark TMMark_${type}">]</span>`;
687
+ if (linkLabel.length >= 3) {
688
+ output = output.concat(`<span class="TMMark TMMark_${type}">${linkLabel[0]}</span>`, `<span class="${label}">${linkLabel[1]}</span>`, `<span class="TMMark TMMark_${type}">${linkLabel[2]}</span>`);
689
+ }
690
+ return {
691
+ output: output,
692
+ charCount: currentOffset
693
+ };
694
+ } else if (linkDetails) {
695
+ // Inline link
543
696
 
544
- if (string.match(_grammar.inlineGrammar.imageOpen.regexp)) {
545
- // Opening image. It's okay if this is a matching pair of brackets
546
- bracketLevel++;
547
- currentOffset += 2;
548
- continue textOuter;
549
- } // Check for link (not an image because that would have been captured and skipped over above)
697
+ // This should never happen, but better safe than sorry.
698
+ while (linkDetails.length < 7) {
699
+ linkDetails.push('');
700
+ }
701
+ return {
702
+ 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>`,
703
+ charCount: currentOffset
704
+ };
705
+ }
706
+ return false;
707
+ }
550
708
 
709
+ /**
710
+ * Formats a markdown string as HTML, using Markdown inline formatting.
711
+ * @param {string} originalString The input (markdown inline formatted) string
712
+ * @returns {string} The HTML formatted output
713
+ */
714
+ processInlineStyles(originalString) {
715
+ let processed = '';
716
+ let stack = []; // Stack is an array of objects of the format: {delimiter, delimString, count, output}
717
+ let offset = 0;
718
+ let string = originalString;
719
+ outer: while (string) {
720
+ // Process simple rules (non-delimiter)
721
+ for (let rule of ['escape', 'code', 'autolink', 'html']) {
722
+ let cap = _grammar.inlineGrammar[rule].regexp.exec(string);
723
+ if (cap) {
724
+ string = string.substr(cap[0].length);
725
+ offset += cap[0].length;
726
+ processed += _grammar.inlineGrammar[rule].replacement
727
+ // .replace(/\$\$([1-9])/g, (str, p1) => processInlineStyles(cap[p1])) // todo recursive calling
728
+ .replace(/\$([1-9])/g, (str, p1) => (0, _grammar.htmlescape)(cap[p1]));
729
+ continue outer;
730
+ }
731
+ }
551
732
 
552
- if (string.match(_grammar.inlineGrammar.linkOpen.regexp)) {
553
- // Opening bracket. Two things to do:
554
- // 1) it's okay if this part of a pair of brackets.
555
- // 2) If we are currently trying to parse a link, this nested bracket musn't start a valid link (no nested links allowed)
556
- bracketLevel++; // if (bracketLevel >= 2) return false; // Nested unescaped brackets, this doesn't qualify as a link / image
733
+ // Check for links / images
734
+ let potentialLink = string.match(_grammar.inlineGrammar.linkOpen.regexp);
735
+ let potentialImage = string.match(_grammar.inlineGrammar.imageOpen.regexp);
736
+ if (potentialImage || potentialLink) {
737
+ let result = this.parseLinkOrImage(string, potentialImage);
738
+ if (result) {
739
+ processed = `${processed}${result.output}`;
740
+ string = string.substr(result.charCount);
741
+ offset += result.charCount;
742
+ continue outer;
743
+ }
744
+ }
557
745
 
558
- if (!isImage) {
559
- if (this.parseLinkOrImage(string, false)) {
560
- // Valid link inside this possible link, which makes this link invalid (inner links beat outer ones)
561
- return false;
562
- }
563
- }
746
+ // Check for em / strong delimiters
747
+ let cap = /(^\*+)|(^_+)/.exec(string);
748
+ if (cap) {
749
+ let delimCount = cap[0].length;
750
+ const delimString = cap[0];
751
+ const currentDelimiter = cap[0][0]; // This should be * or _
564
752
 
565
- currentOffset += 1;
566
- continue textOuter;
567
- } // Check for closing bracket
753
+ string = string.substr(cap[0].length);
568
754
 
755
+ // We have a delimiter run. Let's check if it can open or close an emphasis.
569
756
 
570
- if (string.match(/^\]/)) {
571
- bracketLevel--;
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 : ' ';
759
+ const punctuationFollows = following.match(_grammar.punctuationLeading);
760
+ const punctuationPrecedes = preceding.match(_grammar.punctuationTrailing);
761
+ const whitespaceFollows = following.match(/^\s/);
762
+ const whitespacePrecedes = preceding.match(/\s$/);
572
763
 
573
- if (bracketLevel == 0) {
574
- // Found matching bracket and haven't found anything disqualifying this as link / image.
575
- linkText = originalString.substr(textOffset, currentOffset - textOffset);
576
- currentOffset++;
577
- continue textOuter;
578
- }
579
- } // Nothing matches, proceed to next char
764
+ // These are the rules for right-flanking and left-flanking delimiter runs as per CommonMark spec
765
+ let canOpen = !whitespaceFollows && (!punctuationFollows || !!whitespacePrecedes || !!punctuationPrecedes);
766
+ let canClose = !whitespacePrecedes && (!punctuationPrecedes || !!whitespaceFollows || !!punctuationFollows);
580
767
 
768
+ // Underscores have more detailed rules than just being part of left- or right-flanking run:
769
+ if (currentDelimiter == '_' && canOpen && canClose) {
770
+ canOpen = punctuationPrecedes;
771
+ canClose = punctuationFollows;
772
+ }
581
773
 
582
- currentOffset++;
583
- } // Did we find a link text (i.e., find a matching closing bracket?)
774
+ // If the delimiter can close, check the stack if there's something it can close
775
+ if (canClose) {
776
+ let stackPointer = stack.length - 1;
777
+ // See if we can find a matching opening delimiter, move down through the stack
778
+ while (delimCount && stackPointer >= 0) {
779
+ if (stack[stackPointer].delimiter == currentDelimiter) {
780
+ // We found a matching delimiter, let's construct the formatted string
584
781
 
782
+ // Firstly, if we skipped any stack levels, pop them immediately (non-matching delimiters)
783
+ while (stackPointer < stack.length - 1) {
784
+ const entry = stack.pop();
785
+ processed = `${entry.output}${entry.delimString.substr(0, entry.count)}${processed}`;
786
+ }
585
787
 
586
- if (linkText === false) return false; // Nope
587
- // So far, so good. We've got a valid link text. Let's see what type of link this is
588
-
589
- var nextChar = currentOffset < originalString.length ? originalString.substr(currentOffset, 1) : ''; // REFERENCE LINKS
590
-
591
- if (nextChar == '[') {
592
- var _string = originalString.substr(currentOffset);
593
-
594
- var _cap = _grammar.inlineGrammar.linkLabel.regexp.exec(_string);
595
-
596
- if (_cap) {
597
- // Valid link label
598
- currentOffset += _cap[0].length;
599
- linkLabel.push(_cap[1], _cap[2], _cap[3]);
600
-
601
- if (_cap[_grammar.inlineGrammar.linkLabel.labelPlaceholder]) {
602
- // Full reference link
603
- linkRef = _cap[_grammar.inlineGrammar.linkLabel.labelPlaceholder];
604
- } else {
605
- // Collapsed reference link
606
- linkRef = linkText.trim();
607
- }
608
- } else {
609
- // Not a valid link label
610
- return false;
611
- }
612
- } else if (nextChar != '(') {
613
- // Shortcut ref link
614
- linkRef = linkText.trim(); // INLINE LINKS
615
- } else {
616
- // nextChar == '('
617
- // Potential inline link
618
- currentOffset++;
619
- var parenthesisLevel = 1;
620
-
621
- inlineOuter: while (currentOffset < originalString.length && parenthesisLevel > 0) {
622
- var _string2 = originalString.substr(currentOffset); // Process whitespace
623
-
624
-
625
- var _cap2 = /^\s+/.exec(_string2);
626
-
627
- if (_cap2) {
628
- switch (linkDetails.length) {
629
- case 0:
630
- linkDetails.push(_cap2[0]);
631
- break;
632
- // Opening whitespace
633
-
634
- case 1:
635
- linkDetails.push(_cap2[0]);
636
- break;
637
- // Open destination, but not a destination yet; desination opened with <
638
-
639
- case 2:
640
- // Open destination with content in it. Whitespace only allowed if opened by angle bracket, otherwise this closes the destination
641
- if (linkDetails[0].match(/</)) {
642
- linkDetails[1] = linkDetails[1].concat(_cap2[0]);
643
- } else {
644
- if (parenthesisLevel != 1) return false; // Unbalanced parenthesis
645
-
646
- linkDetails.push(''); // Empty end delimiter for destination
647
-
648
- linkDetails.push(_cap2[0]); // Whitespace in between destination and title
649
- }
650
-
651
- break;
652
-
653
- case 3:
654
- linkDetails.push(_cap2[0]);
655
- break;
656
- // Whitespace between destination and title
657
-
658
- case 4:
659
- return false;
660
- // This should never happen (no opener for title yet, but more whitespace to capture)
661
-
662
- case 5:
663
- linkDetails.push('');
664
- // Whitespace at beginning of title, push empty title and continue
665
-
666
- case 6:
667
- linkDetails[5] = linkDetails[5].concat(_cap2[0]);
668
- break;
669
- // Whitespace in title
670
-
671
- case 7:
672
- linkDetails[6] = linkDetails[6].concat(_cap2[0]);
673
- break;
674
- // Whitespace after closing delimiter
675
-
676
- default:
677
- return false;
678
- // We should never get here
679
- }
680
-
681
- currentOffset += _cap2[0].length;
682
- continue inlineOuter;
683
- } // Process backslash escapes
684
-
685
-
686
- _cap2 = _grammar.inlineGrammar.escape.regexp.exec(_string2);
687
-
688
- if (_cap2) {
689
- switch (linkDetails.length) {
690
- case 0:
691
- linkDetails.push('');
692
- // this opens the link destination, add empty opening delimiter and proceed to next case
693
-
694
- case 1:
695
- linkDetails.push(_cap2[0]);
696
- break;
697
- // This opens the link destination, append it
698
-
699
- case 2:
700
- linkDetails[1] = linkDetails[1].concat(_cap2[0]);
701
- break;
702
- // Part of the link destination
703
-
704
- case 3:
705
- return false;
706
- // Lacking opening delimiter for link title
707
-
708
- case 4:
709
- return false;
710
- // Lcaking opening delimiter for link title
711
-
712
- case 5:
713
- linkDetails.push('');
714
- // This opens the link title
715
-
716
- case 6:
717
- linkDetails[5] = linkDetails[5].concat(_cap2[0]);
718
- break;
719
- // Part of the link title
720
-
721
- default:
722
- return false;
723
- // After link title was closed, without closing parenthesis
724
- }
725
-
726
- currentOffset += _cap2[0].length;
727
- continue inlineOuter;
728
- } // Process opening angle bracket as deilimiter of destination
729
-
730
-
731
- if (linkDetails.length < 2 && _string2.match(/^</)) {
732
- if (linkDetails.length == 0) linkDetails.push('');
733
- linkDetails[0] = linkDetails[0].concat('<');
734
- currentOffset++;
735
- continue inlineOuter;
736
- } // Process closing angle bracket as delimiter of destination
737
-
738
-
739
- if ((linkDetails.length == 1 || linkDetails.length == 2) && _string2.match(/^>/)) {
740
- if (linkDetails.length == 1) linkDetails.push(''); // Empty link destination
741
-
742
- linkDetails.push('>');
743
- currentOffset++;
744
- continue inlineOuter;
745
- } // Process non-parenthesis delimiter for title.
746
-
747
-
748
- _cap2 = /^["']/.exec(_string2); // For this to be a valid opener, we have to either have no destination, only whitespace so far,
749
- // or a destination with trailing whitespace.
750
-
751
- if (_cap2 && (linkDetails.length == 0 || linkDetails.length == 1 || linkDetails.length == 4)) {
752
- while (linkDetails.length < 4) {
753
- linkDetails.push('');
754
- }
755
-
756
- linkDetails.push(_cap2[0]);
757
- currentOffset++;
758
- continue inlineOuter;
759
- } // 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
760
-
761
-
762
- if (_cap2 && (linkDetails.length == 5 || linkDetails.length == 6) && linkDetails[4] == _cap2[0]) {
763
- if (linkDetails.length == 5) linkDetails.push(''); // Empty link title
764
-
765
- linkDetails.push(_cap2[0]);
766
- currentOffset++;
767
- continue inlineOuter;
768
- } // Other cases (linkDetails.length == 2, 3, 7) will be handled with the "default" case below.
769
- // Process opening parenthesis
770
-
771
-
772
- if (_string2.match(/^\(/)) {
773
- switch (linkDetails.length) {
774
- case 0:
775
- linkDetails.push('');
776
- // this opens the link destination, add empty opening delimiter and proceed to next case
777
-
778
- case 1:
779
- linkDetails.push('');
780
- // This opens the link destination
781
-
782
- case 2:
783
- // Part of the link destination
784
- linkDetails[1] = linkDetails[1].concat('(');
785
- if (!linkDetails[0].match(/<$/)) parenthesisLevel++;
786
- break;
787
-
788
- case 3:
789
- linkDetails.push('');
790
- // opening delimiter for link title
791
-
792
- case 4:
793
- linkDetails.push('(');
794
- break;
795
- // opening delimiter for link title
796
-
797
- case 5:
798
- linkDetails.push('');
799
- // opens the link title, add empty title content and proceed to next case
800
-
801
- case 6:
802
- // Part of the link title. Un-escaped parenthesis only allowed in " or ' delimited title
803
- if (linkDetails[4] == '(') return false;
804
- linkDetails[5] = linkDetails[5].concat('(');
805
- break;
806
-
807
- default:
808
- return false;
809
- // After link title was closed, without closing parenthesis
810
- }
811
-
812
- currentOffset++;
813
- continue inlineOuter;
814
- } // Process closing parenthesis
815
-
816
-
817
- if (_string2.match(/^\)/)) {
818
- if (linkDetails.length <= 2) {
819
- // We are inside the link destination. Parentheses have to be matched if not in angle brackets
820
- while (linkDetails.length < 2) {
821
- linkDetails.push('');
822
- }
823
-
824
- if (!linkDetails[0].match(/<$/)) parenthesisLevel--;
825
-
826
- if (parenthesisLevel > 0) {
827
- linkDetails[1] = linkDetails[1].concat(')');
828
- }
829
- } else if (linkDetails.length == 5 || linkDetails.length == 6) {
830
- // We are inside the link title.
831
- if (linkDetails[4] == '(') {
832
- // This closes the link title
833
- if (linkDetails.length == 5) linkDetails.push('');
834
- linkDetails.push(')');
788
+ // Then, format the string
789
+ if (delimCount >= 2 && stack[stackPointer].count >= 2) {
790
+ // Strong
791
+ processed = `<span class="TMMark">${currentDelimiter}${currentDelimiter}</span><strong class="TMStrong">${processed}</strong><span class="TMMark">${currentDelimiter}${currentDelimiter}</span>`;
792
+ delimCount -= 2;
793
+ stack[stackPointer].count -= 2;
835
794
  } else {
836
- // Just regular ol' content
837
- if (linkDetails.length == 5) linkDetails.push(')');else linkDetails[5] = linkDetails[5].concat(')');
795
+ // Em
796
+ processed = `<span class="TMMark">${currentDelimiter}</span><em class="TMEm">${processed}</em><span class="TMMark">${currentDelimiter}</span>`;
797
+ delimCount -= 1;
798
+ stack[stackPointer].count -= 1;
838
799
  }
839
- } else {
840
- parenthesisLevel--; // This should decrease it from 1 to 0...
841
- }
842
800
 
843
- if (parenthesisLevel == 0) {
844
- // No invalid condition, let's make sure the linkDetails array is complete
845
- while (linkDetails.length < 7) {
846
- linkDetails.push('');
801
+ // If that stack level is empty now, pop it
802
+ if (stack[stackPointer].count == 0) {
803
+ let entry = stack.pop();
804
+ processed = `${entry.output}${processed}`;
805
+ stackPointer--;
847
806
  }
848
- }
849
-
850
- currentOffset++;
851
- continue inlineOuter;
852
- } // Any old character
853
-
854
-
855
- _cap2 = /^./.exec(_string2);
856
-
857
- if (_cap2) {
858
- switch (linkDetails.length) {
859
- case 0:
860
- linkDetails.push('');
861
- // this opens the link destination, add empty opening delimiter and proceed to next case
862
-
863
- case 1:
864
- linkDetails.push(_cap2[0]);
865
- break;
866
- // This opens the link destination, append it
867
-
868
- case 2:
869
- linkDetails[1] = linkDetails[1].concat(_cap2[0]);
870
- break;
871
- // Part of the link destination
872
-
873
- case 3:
874
- return false;
875
- // Lacking opening delimiter for link title
876
-
877
- case 4:
878
- return false;
879
- // Lcaking opening delimiter for link title
880
-
881
- case 5:
882
- linkDetails.push('');
883
- // This opens the link title
884
-
885
- case 6:
886
- linkDetails[5] = linkDetails[5].concat(_cap2[0]);
887
- break;
888
- // Part of the link title
889
-
890
- default:
891
- return false;
892
- // After link title was closed, without closing parenthesis
893
- }
894
-
895
- currentOffset += _cap2[0].length;
896
- continue inlineOuter;
897
- }
898
-
899
- throw "Infinite loop"; // we should never get here since the last test matches any character
900
- }
901
-
902
- if (parenthesisLevel > 0) return false; // Parenthes(es) not closed
903
- }
904
-
905
- if (linkRef !== false) {
906
- // Ref link; check that linkRef is valid
907
- var valid = false;
908
-
909
- var _iterator2 = _createForOfIteratorHelper(this.linkLabels),
910
- _step2;
911
-
912
- try {
913
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
914
- var _label = _step2.value;
915
-
916
- if (_label == linkRef) {
917
- valid = true;
918
- break;
807
+ } else {
808
+ // This stack level's delimiter type doesn't match the current delimiter type
809
+ // Go down one level in the stack
810
+ stackPointer--;
919
811
  }
920
812
  }
921
- } catch (err) {
922
- _iterator2.e(err);
923
- } finally {
924
- _iterator2.f();
925
813
  }
926
-
927
- var label = valid ? "TMLinkLabel TMLinkLabel_Valid" : "TMLinkLabel TMLinkLabel_Invalid";
928
- var output = "<span class=\"TMMark TMMark_".concat(type, "\">").concat(opener, "</span><span class=\"").concat(type, " ").concat(linkLabel.length < 3 || !linkLabel[1] ? label : "", "\">").concat(this.processInlineStyles(linkText), "</span><span class=\"TMMark TMMark_").concat(type, "\">]</span>");
929
-
930
- if (linkLabel.length >= 3) {
931
- output = output.concat("<span class=\"TMMark TMMark_".concat(type, "\">").concat(linkLabel[0], "</span>"), "<span class=\"".concat(label, "\">").concat(linkLabel[1], "</span>"), "<span class=\"TMMark TMMark_".concat(type, "\">").concat(linkLabel[2], "</span>"));
814
+ // If there are still delimiters left, and the delimiter run can open, push it on the stack
815
+ if (delimCount && canOpen) {
816
+ stack.push({
817
+ delimiter: currentDelimiter,
818
+ delimString: delimString,
819
+ count: delimCount,
820
+ output: processed
821
+ });
822
+ processed = ''; // Current formatted output has been pushed on the stack and will be prepended when the stack gets popped
823
+ delimCount = 0;
932
824
  }
933
825
 
934
- return {
935
- output: output,
936
- charCount: currentOffset
937
- };
938
- } else if (linkDetails) {
939
- // Inline link
940
- // This should never happen, but better safe than sorry.
941
- while (linkDetails.length < 7) {
942
- linkDetails.push('');
826
+ // Any delimiters that are left (closing unmatched) are appended to the output.
827
+ if (delimCount) {
828
+ processed = `${processed}${delimString.substr(0, delimCount)}`;
943
829
  }
944
-
945
- return {
946
- output: "<span class=\"TMMark TMMark_".concat(type, "\">").concat(opener, "</span><span class=\"").concat(type, "\">").concat(this.processInlineStyles(linkText), "</span><span class=\"TMMark TMMark_").concat(type, "\">](").concat(linkDetails[0], "</span><span class=\"").concat(type, "Destination\">").concat(linkDetails[1], "</span><span class=\"TMMark TMMark_").concat(type, "\">").concat(linkDetails[2]).concat(linkDetails[3]).concat(linkDetails[4], "</span><span class=\"").concat(type, "Title\">").concat(linkDetails[5], "</span><span class=\"TMMark TMMark_").concat(type, "\">").concat(linkDetails[6], ")</span>"),
947
- charCount: currentOffset
948
- };
830
+ offset += cap[0].length;
831
+ continue outer;
949
832
  }
950
833
 
951
- return false;
952
- }
953
- /**
954
- * Formats a markdown string as HTML, using Markdown inline formatting.
955
- * @param {string} originalString The input (markdown inline formatted) string
956
- * @returns {string} The HTML formatted output
957
- */
958
-
959
- }, {
960
- key: "processInlineStyles",
961
- value: function processInlineStyles(originalString) {
962
- var _this3 = this;
963
-
964
- var processed = '';
965
- var stack = []; // Stack is an array of objects of the format: {delimiter, delimString, count, output}
966
-
967
- var offset = 0;
968
- var string = originalString;
969
-
970
- var _loop = function _loop() {
971
- var _loop2 = function _loop2() {
972
- var rule = _arr2[_i2];
973
-
974
- var cap = _grammar.inlineGrammar[rule].regexp.exec(string);
975
-
976
- if (cap) {
977
- string = string.substr(cap[0].length);
978
- offset += cap[0].length;
979
- processed += _grammar.inlineGrammar[rule].replacement // .replace(/\$\$([1-9])/g, (str, p1) => processInlineStyles(cap[p1])) // todo recursive calling
980
- .replace(/\$([1-9])/g, function (str, p1) {
981
- return (0, _grammar.htmlescape)(cap[p1]);
982
- });
983
- return {
984
- v: "continue|outer"
985
- };
986
- }
987
- };
988
-
989
- // Process simple rules (non-delimiter)
990
- for (var _i2 = 0, _arr2 = ['escape', 'code', 'autolink', 'html']; _i2 < _arr2.length; _i2++) {
991
- var _ret2 = _loop2();
992
-
993
- if (_typeof(_ret2) === "object") return _ret2.v;
994
- } // Check for links / images
995
-
996
-
997
- var potentialLink = string.match(_grammar.inlineGrammar.linkOpen.regexp);
998
- var potentialImage = string.match(_grammar.inlineGrammar.imageOpen.regexp);
999
-
1000
- if (potentialImage || potentialLink) {
1001
- var result = _this3.parseLinkOrImage(string, potentialImage);
1002
-
1003
- if (result) {
1004
- processed = "".concat(processed).concat(result.output);
1005
- string = string.substr(result.charCount);
1006
- offset += result.charCount;
1007
- return "continue|outer";
1008
- }
1009
- } // Check for em / strong delimiters
1010
-
1011
-
1012
- var cap = /(^\*+)|(^_+)/.exec(string);
1013
-
1014
- if (cap) {
1015
- var delimCount = cap[0].length;
1016
- var delimString = cap[0];
1017
- var currentDelimiter = cap[0][0]; // This should be * or _
1018
-
1019
- string = string.substr(cap[0].length); // We have a delimiter run. Let's check if it can open or close an emphasis.
1020
-
1021
- var preceding = offset > 0 ? originalString.substr(0, offset) : ' '; // beginning and end of line count as whitespace
1022
-
1023
- var following = offset + cap[0].length < originalString.length ? string : ' ';
1024
- var punctuationFollows = following.match(_grammar.punctuationLeading);
1025
- var punctuationPrecedes = preceding.match(_grammar.punctuationTrailing);
1026
- var whitespaceFollows = following.match(/^\s/);
1027
- var whitespacePrecedes = preceding.match(/\s$/); // These are the rules for right-flanking and left-flanking delimiter runs as per CommonMark spec
1028
-
1029
- var canOpen = !whitespaceFollows && (!punctuationFollows || !!whitespacePrecedes || !!punctuationPrecedes);
1030
- var canClose = !whitespacePrecedes && (!punctuationPrecedes || !!whitespaceFollows || !!punctuationFollows); // Underscores have more detailed rules than just being part of left- or right-flanking run:
1031
-
1032
- if (currentDelimiter == '_' && canOpen && canClose) {
1033
- canOpen = punctuationPrecedes;
1034
- canClose = punctuationFollows;
1035
- } // If the delimiter can close, check the stack if there's something it can close
1036
-
1037
-
1038
- if (canClose) {
1039
- var stackPointer = stack.length - 1; // See if we can find a matching opening delimiter, move down through the stack
1040
-
1041
- while (delimCount && stackPointer >= 0) {
1042
- if (stack[stackPointer].delimiter == currentDelimiter) {
1043
- // We found a matching delimiter, let's construct the formatted string
1044
- // Firstly, if we skipped any stack levels, pop them immediately (non-matching delimiters)
1045
- while (stackPointer < stack.length - 1) {
1046
- var _entry = stack.pop();
1047
-
1048
- processed = "".concat(_entry.output).concat(_entry.delimString.substr(0, _entry.count)).concat(processed);
1049
- } // Then, format the string
1050
-
1051
-
1052
- if (delimCount >= 2 && stack[stackPointer].count >= 2) {
1053
- // Strong
1054
- processed = "<span class=\"TMMark\">".concat(currentDelimiter).concat(currentDelimiter, "</span><strong class=\"TMStrong\">").concat(processed, "</strong><span class=\"TMMark\">").concat(currentDelimiter).concat(currentDelimiter, "</span>");
1055
- delimCount -= 2;
1056
- stack[stackPointer].count -= 2;
1057
- } else {
1058
- // Em
1059
- processed = "<span class=\"TMMark\">".concat(currentDelimiter, "</span><em class=\"TMEm\">").concat(processed, "</em><span class=\"TMMark\">").concat(currentDelimiter, "</span>");
1060
- delimCount -= 1;
1061
- stack[stackPointer].count -= 1;
1062
- } // If that stack level is empty now, pop it
1063
-
1064
-
1065
- if (stack[stackPointer].count == 0) {
1066
- var _entry2 = stack.pop();
1067
-
1068
- processed = "".concat(_entry2.output).concat(processed);
1069
- stackPointer--;
1070
- }
1071
- } else {
1072
- // This stack level's delimiter type doesn't match the current delimiter type
1073
- // Go down one level in the stack
1074
- stackPointer--;
1075
- }
834
+ // Check for strikethrough delimiter
835
+ cap = /^~~/.exec(string);
836
+ if (cap) {
837
+ let consumed = false;
838
+ let stackPointer = stack.length - 1;
839
+ // See if we can find a matching opening delimiter, move down through the stack
840
+ while (!consumed && stackPointer >= 0) {
841
+ if (stack[stackPointer].delimiter == '~') {
842
+ // We found a matching delimiter, let's construct the formatted string
843
+
844
+ // Firstly, if we skipped any stack levels, pop them immediately (non-matching delimiters)
845
+ while (stackPointer < stack.length - 1) {
846
+ const entry = stack.pop();
847
+ processed = `${entry.output}${entry.delimString.substr(0, entry.count)}${processed}`;
1076
848
  }
1077
- } // If there are still delimiters left, and the delimiter run can open, push it on the stack
1078
-
1079
-
1080
- if (delimCount && canOpen) {
1081
- stack.push({
1082
- delimiter: currentDelimiter,
1083
- delimString: delimString,
1084
- count: delimCount,
1085
- output: processed
1086
- });
1087
- processed = ''; // Current formatted output has been pushed on the stack and will be prepended when the stack gets popped
1088
-
1089
- delimCount = 0;
1090
- } // Any delimiters that are left (closing unmatched) are appended to the output.
1091
-
1092
-
1093
- if (delimCount) {
1094
- processed = "".concat(processed).concat(delimString.substr(0, delimCount));
1095
- }
1096
-
1097
- offset += cap[0].length;
1098
- return "continue|outer";
1099
- } // Check for strikethrough delimiter
1100
-
1101
-
1102
- cap = /^~~/.exec(string);
1103
-
1104
- if (cap) {
1105
- var consumed = false;
1106
-
1107
- var _stackPointer = stack.length - 1; // See if we can find a matching opening delimiter, move down through the stack
1108
-
1109
-
1110
- while (!consumed && _stackPointer >= 0) {
1111
- if (stack[_stackPointer].delimiter == '~') {
1112
- // We found a matching delimiter, let's construct the formatted string
1113
- // Firstly, if we skipped any stack levels, pop them immediately (non-matching delimiters)
1114
- while (_stackPointer < stack.length - 1) {
1115
- var _entry4 = stack.pop();
1116
-
1117
- processed = "".concat(_entry4.output).concat(_entry4.delimString.substr(0, _entry4.count)).concat(processed);
1118
- } // Then, format the string
1119
-
1120
-
1121
- processed = "<span class=\"TMMark\">~~</span><del class=\"TMStrikethrough\">".concat(processed, "</del><span class=\"TMMark\">~~</span>");
1122
-
1123
- var _entry3 = stack.pop();
1124
-
1125
- processed = "".concat(_entry3.output).concat(processed);
1126
- consumed = true;
1127
- } else {
1128
- // This stack level's delimiter type doesn't match the current delimiter type
1129
- // Go down one level in the stack
1130
- _stackPointer--;
1131
- }
1132
- } // If there are still delimiters left, and the delimiter run can open, push it on the stack
1133
-
1134
849
 
1135
- if (!consumed) {
1136
- stack.push({
1137
- delimiter: '~',
1138
- delimString: '~~',
1139
- count: 2,
1140
- output: processed
1141
- });
1142
- processed = ''; // Current formatted output has been pushed on the stack and will be prepended when the stack gets popped
850
+ // Then, format the string
851
+ processed = `<span class="TMMark">~~</span><del class="TMStrikethrough">${processed}</del><span class="TMMark">~~</span>`;
852
+ let entry = stack.pop();
853
+ processed = `${entry.output}${processed}`;
854
+ consumed = true;
855
+ } else {
856
+ // This stack level's delimiter type doesn't match the current delimiter type
857
+ // Go down one level in the stack
858
+ stackPointer--;
1143
859
  }
860
+ }
1144
861
 
1145
- offset += cap[0].length;
1146
- string = string.substr(cap[0].length);
1147
- return "continue|outer";
1148
- } // Process 'default' rule
1149
-
1150
-
1151
- cap = _grammar.inlineGrammar.default.regexp.exec(string);
1152
-
1153
- if (cap) {
1154
- string = string.substr(cap[0].length);
1155
- offset += cap[0].length;
1156
- processed += _grammar.inlineGrammar.default.replacement.replace(/\$([1-9])/g, function (str, p1) {
1157
- return (0, _grammar.htmlescape)(cap[p1]);
862
+ // If there are still delimiters left, and the delimiter run can open, push it on the stack
863
+ if (!consumed) {
864
+ stack.push({
865
+ delimiter: '~',
866
+ delimString: '~~',
867
+ count: 2,
868
+ output: processed
1158
869
  });
1159
- return "continue|outer";
870
+ processed = ''; // Current formatted output has been pushed on the stack and will be prepended when the stack gets popped
1160
871
  }
1161
872
 
1162
- throw 'Infinite loop!';
1163
- };
1164
-
1165
- outer: while (string) {
1166
- var _ret = _loop();
1167
-
1168
- if (_ret === "continue|outer") continue outer;
1169
- } // Empty the stack, any opening delimiters are unused
1170
-
1171
-
1172
- while (stack.length) {
1173
- var entry = stack.pop();
1174
- processed = "".concat(entry.output).concat(entry.delimString.substr(0, entry.count)).concat(processed);
873
+ offset += cap[0].length;
874
+ string = string.substr(cap[0].length);
875
+ continue outer;
1175
876
  }
1176
877
 
1177
- return processed;
1178
- }
1179
- /**
1180
- * Clears the line dirty flag (resets it to an array of false)
1181
- */
1182
-
1183
- }, {
1184
- key: "clearDirtyFlag",
1185
- value: function clearDirtyFlag() {
1186
- this.lineDirty = new Array(this.lines.length);
1187
-
1188
- for (var i = 0; i < this.lineDirty.length; i++) {
1189
- this.lineDirty[i] = false;
878
+ // Process 'default' rule
879
+ cap = _grammar.inlineGrammar.default.regexp.exec(string);
880
+ if (cap) {
881
+ string = string.substr(cap[0].length);
882
+ offset += cap[0].length;
883
+ processed += _grammar.inlineGrammar.default.replacement.replace(/\$([1-9])/g, (str, p1) => (0, _grammar.htmlescape)(cap[p1]));
884
+ continue outer;
1190
885
  }
886
+ throw 'Infinite loop!';
1191
887
  }
1192
- /**
1193
- * Updates the class properties (lines, lineElements) from the DOM.
1194
- * @returns true if contents changed
1195
- */
1196
-
1197
- }, {
1198
- key: "updateLineContents",
1199
- value: function updateLineContents() {
1200
- // this.lineDirty = [];
1201
- // Check if we have changed anything about the number of lines (inserted or deleted a paragraph)
1202
- // < 0 means line(s) removed; > 0 means line(s) added
1203
- var lineDelta = this.e.childElementCount - this.lines.length;
1204
-
1205
- if (lineDelta) {
1206
- // yup. Let's try how much we can salvage (find out which lines from beginning and end were unchanged)
1207
- // Find lines from the beginning that haven't changed...
1208
- var firstChangedLine = 0;
1209
-
1210
- while (firstChangedLine <= this.lines.length && firstChangedLine <= this.lineElements.length && this.lineElements[firstChangedLine] // Check that the line element hasn't been deleted
1211
- && this.lines[firstChangedLine] == this.lineElements[firstChangedLine].textContent) {
1212
- firstChangedLine++;
1213
- } // End also from the end
1214
-
1215
-
1216
- var lastChangedLine = -1;
1217
-
1218
- while (-lastChangedLine < this.lines.length && -lastChangedLine < this.lineElements.length && this.lines[this.lines.length + lastChangedLine] == this.lineElements[this.lineElements.length + lastChangedLine].textContent) {
1219
- lastChangedLine--;
1220
- }
1221
-
1222
- var linesToDelete = this.lines.length + lastChangedLine + 1 - firstChangedLine;
1223
- if (linesToDelete < -lineDelta) linesToDelete = -lineDelta;
1224
- if (linesToDelete < 0) linesToDelete = 0;
1225
- var linesToAdd = [];
1226
888
 
1227
- for (var l = 0; l < linesToDelete + lineDelta; l++) {
1228
- linesToAdd.push(this.lineElements[firstChangedLine + l].textContent);
1229
- }
1230
-
1231
- this.spliceLines(firstChangedLine, linesToDelete, linesToAdd, false);
1232
- } else {
1233
- // No lines added or removed
1234
- for (var line = 0; line < this.lineElements.length; line++) {
1235
- var e = this.lineElements[line];
1236
- var ct = e.textContent;
1237
-
1238
- if (this.lines[line] !== ct) {
1239
- // Line changed, update it
1240
- this.lines[line] = ct;
1241
- this.lineDirty[line] = true;
1242
- }
1243
- }
1244
- }
889
+ // Empty the stack, any opening delimiters are unused
890
+ while (stack.length) {
891
+ const entry = stack.pop();
892
+ processed = `${entry.output}${entry.delimString.substr(0, entry.count)}${processed}`;
1245
893
  }
1246
- /**
1247
- * Processes a new paragraph.
1248
- * @param sel The current selection
1249
- */
1250
-
1251
- }, {
1252
- key: "processNewParagraph",
1253
- value: function processNewParagraph(sel) {
1254
- if (!sel) return; // Update lines from content
1255
-
1256
- this.updateLineContents();
1257
- var continuableType = false; // Let's see if we need to continue a list
1258
-
1259
- var checkLine = sel.col > 0 ? sel.row : sel.row - 1;
1260
-
1261
- switch (this.lineTypes[checkLine]) {
1262
- case 'TMUL':
1263
- continuableType = 'TMUL';
1264
- break;
894
+ return processed;
895
+ }
1265
896
 
1266
- case 'TMOL':
1267
- continuableType = 'TMOL';
1268
- break;
897
+ /**
898
+ * Clears the line dirty flag (resets it to an array of false)
899
+ */
900
+ clearDirtyFlag() {
901
+ this.lineDirty = new Array(this.lines.length);
902
+ for (let i = 0; i < this.lineDirty.length; i++) {
903
+ this.lineDirty[i] = false;
904
+ }
905
+ }
1269
906
 
1270
- case 'TMIndentedCode':
1271
- continuableType = 'TMIndentedCode';
1272
- break;
907
+ /**
908
+ * Updates the class properties (lines, lineElements) from the DOM.
909
+ * @returns true if contents changed
910
+ */
911
+ updateLineContents() {
912
+ // this.lineDirty = [];
913
+ // Check if we have changed anything about the number of lines (inserted or deleted a paragraph)
914
+ // < 0 means line(s) removed; > 0 means line(s) added
915
+ let lineDelta = this.e.childElementCount - this.lines.length;
916
+ if (lineDelta) {
917
+ // yup. Let's try how much we can salvage (find out which lines from beginning and end were unchanged)
918
+ // Find lines from the beginning that haven't changed...
919
+ 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) {
922
+ firstChangedLine++;
1273
923
  }
1274
924
 
1275
- var lines = this.lines[sel.row].replace(/\n\n$/, '\n').split(/(?:\r\n|\n|\r)/);
1276
-
1277
- if (lines.length == 1) {
1278
- // No new line
1279
- this.updateFormatting();
1280
- return;
925
+ // End also from the end
926
+ let lastChangedLine = -1;
927
+ while (-lastChangedLine < this.lines.length && -lastChangedLine < this.lineElements.length && this.lines[this.lines.length + lastChangedLine] == this.lineElements[this.lineElements.length + lastChangedLine].textContent) {
928
+ lastChangedLine--;
1281
929
  }
1282
-
1283
- this.spliceLines(sel.row, 1, lines, true);
1284
- sel.row++;
1285
- sel.col = 0;
1286
-
1287
- if (continuableType) {
1288
- // Check if the previous line was non-empty
1289
- var capture = _grammar.lineGrammar[continuableType].regexp.exec(this.lines[sel.row - 1]);
1290
-
1291
- if (capture) {
1292
- // Convention: capture[1] is the line type marker, capture[2] is the content
1293
- if (capture[2]) {
1294
- // Previous line has content, continue the continuable type
1295
- // Hack for OL: increment number
1296
- if (continuableType == 'TMOL') {
1297
- capture[1] = capture[1].replace(/\d{1,9}/, function (result) {
1298
- return parseInt(result[0]) + 1;
1299
- });
1300
- }
1301
-
1302
- this.lines[sel.row] = "".concat(capture[1]).concat(this.lines[sel.row]);
1303
- this.lineDirty[sel.row] = true;
1304
- sel.col = capture[1].length;
1305
- } else {
1306
- // Previous line has no content, remove the continuable type from the previous row
1307
- this.lines[sel.row - 1] = '';
1308
- this.lineDirty[sel.row - 1] = true;
1309
- }
930
+ let linesToDelete = this.lines.length + lastChangedLine + 1 - firstChangedLine;
931
+ if (linesToDelete < -lineDelta) linesToDelete = -lineDelta;
932
+ if (linesToDelete < 0) linesToDelete = 0;
933
+ let linesToAdd = [];
934
+ for (let l = 0; l < linesToDelete + lineDelta; l++) {
935
+ linesToAdd.push(this.lineElements[firstChangedLine + l].textContent);
936
+ }
937
+ this.spliceLines(firstChangedLine, linesToDelete, linesToAdd, false);
938
+ } else {
939
+ // No lines added or removed
940
+ for (let line = 0; line < this.lineElements.length; line++) {
941
+ let e = this.lineElements[line];
942
+ let ct = e.textContent;
943
+ if (this.lines[line] !== ct) {
944
+ // Line changed, update it
945
+ this.lines[line] = ct;
946
+ this.lineDirty[line] = true;
1310
947
  }
1311
948
  }
949
+ }
950
+ }
1312
951
 
952
+ /**
953
+ * Processes a new paragraph.
954
+ * @param sel The current selection
955
+ */
956
+ processNewParagraph(sel) {
957
+ if (!sel) return;
958
+
959
+ // Update lines from content
960
+ this.updateLineContents();
961
+ let continuableType = false;
962
+ // Let's see if we need to continue a list
963
+
964
+ let checkLine = sel.col > 0 ? sel.row : sel.row - 1;
965
+ switch (this.lineTypes[checkLine]) {
966
+ case 'TMUL':
967
+ continuableType = 'TMUL';
968
+ break;
969
+ case 'TMOL':
970
+ continuableType = 'TMOL';
971
+ break;
972
+ case 'TMIndentedCode':
973
+ continuableType = 'TMIndentedCode';
974
+ break;
975
+ }
976
+ let lines = this.lines[sel.row].replace(/\n\n$/, '\n').split(/(?:\r\n|\n|\r)/);
977
+ if (lines.length == 1) {
978
+ // No new line
1313
979
  this.updateFormatting();
1314
- } // /**
1315
- // * Processes a "delete" input action.
1316
- // * @param {object} focus The selection
1317
- // * @param {boolean} forward If true, performs a forward delete, otherwise performs a backward delete
1318
- // */
1319
- // processDelete(focus, forward) {
1320
- // if (!focus) return;
1321
- // let anchor = this.getSelection(true);
1322
- // // Do we have a non-empty selection?
1323
- // if (focus.col != anchor.col || focus.row != anchor.row) {
1324
- // // non-empty. direction doesn't matter.
1325
- // this.paste('', anchor, focus);
1326
- // } else {
1327
- // if (forward) {
1328
- // if (focus.col < this.lines[focus.row].length) this.paste('', {row: focus.row, col: focus.col + 1}, focus);
1329
- // else if (focus.col < this.lines.length) this.paste('', {row: focus.row + 1, col: 0}, focus);
1330
- // // Otherwise, we're at the very end and can't delete forward
1331
- // } else {
1332
- // if (focus.col > 0) this.paste('', {row: focus.row, col: focus.col - 1}, focus);
1333
- // else if (focus.row > 0) this.paste('', {row: focus.row - 1, col: this.lines[focus.row - 1].length - 1}, focus);
1334
- // // Otherwise, we're at the very beginning and can't delete backwards
1335
- // }
1336
- // }
1337
- // }
1338
-
1339
- /**
1340
- * 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).
1341
- *
1342
- * @param {boolean} getAnchor if set to true, gets the selection anchor (start point of the selection), otherwise gets the focus (end point).
1343
- * @return {object} An object representing the selection, with properties col and row.
1344
- */
1345
-
1346
- }, {
1347
- key: "getSelection",
1348
- value: function getSelection() {
1349
- var getAnchor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
1350
- var selection = window.getSelection();
1351
- var startNode = getAnchor ? selection.anchorNode : selection.focusNode;
1352
- if (!startNode) return null;
1353
- var offset = startNode.nodeType === Node.TEXT_NODE ? getAnchor ? selection.anchorOffset : selection.focusOffset : 0;
1354
-
1355
- if (startNode == this.e) {
1356
- return {
1357
- row: 0,
1358
- col: offset
1359
- };
1360
- }
1361
-
1362
- var col = this.computeColumn(startNode, offset);
1363
- if (col === null) return null; // We are outside of the editor
1364
- // Find the row node
1365
-
1366
- var node = startNode;
1367
-
1368
- while (node.parentElement != this.e) {
1369
- node = node.parentElement;
1370
- }
1371
-
1372
- var row = 0; // Check if we can read a line number from the data-line-num attribute.
1373
- // The last condition is a security measure since inserting a new paragraph copies the previous rows' line number
1374
-
1375
- if (node.dataset && node.dataset.lineNum && (!node.previousSibling || node.previousSibling.dataset.lineNum != node.dataset.lineNum)) {
1376
- row = parseInt(node.dataset.lineNum);
1377
- } else {
1378
- while (node.previousSibling) {
1379
- row++;
1380
- node = node.previousSibling;
980
+ return;
981
+ }
982
+ this.spliceLines(sel.row, 1, lines, true);
983
+ sel.row++;
984
+ sel.col = 0;
985
+ if (continuableType) {
986
+ // Check if the previous line was non-empty
987
+ let capture = _grammar.lineGrammar[continuableType].regexp.exec(this.lines[sel.row - 1]);
988
+ if (capture) {
989
+ // Convention: capture[1] is the line type marker, capture[2] is the content
990
+ if (capture[2]) {
991
+ // Previous line has content, continue the continuable type
992
+
993
+ // Hack for OL: increment number
994
+ if (continuableType == 'TMOL') {
995
+ capture[1] = capture[1].replace(/\d{1,9}/, result => {
996
+ return parseInt(result[0]) + 1;
997
+ });
998
+ }
999
+ this.lines[sel.row] = `${capture[1]}${this.lines[sel.row]}`;
1000
+ this.lineDirty[sel.row] = true;
1001
+ sel.col = capture[1].length;
1002
+ } else {
1003
+ // Previous line has no content, remove the continuable type from the previous row
1004
+ this.lines[sel.row - 1] = '';
1005
+ this.lineDirty[sel.row - 1] = true;
1381
1006
  }
1382
1007
  }
1008
+ }
1009
+ this.updateFormatting();
1010
+ }
1383
1011
 
1012
+ // /**
1013
+ // * Processes a "delete" input action.
1014
+ // * @param {object} focus The selection
1015
+ // * @param {boolean} forward If true, performs a forward delete, otherwise performs a backward delete
1016
+ // */
1017
+ // processDelete(focus, forward) {
1018
+ // if (!focus) return;
1019
+ // let anchor = this.getSelection(true);
1020
+ // // Do we have a non-empty selection?
1021
+ // if (focus.col != anchor.col || focus.row != anchor.row) {
1022
+ // // non-empty. direction doesn't matter.
1023
+ // this.paste('', anchor, focus);
1024
+ // } else {
1025
+ // if (forward) {
1026
+ // if (focus.col < this.lines[focus.row].length) this.paste('', {row: focus.row, col: focus.col + 1}, focus);
1027
+ // else if (focus.col < this.lines.length) this.paste('', {row: focus.row + 1, col: 0}, focus);
1028
+ // // Otherwise, we're at the very end and can't delete forward
1029
+ // } else {
1030
+ // if (focus.col > 0) this.paste('', {row: focus.row, col: focus.col - 1}, focus);
1031
+ // else if (focus.row > 0) this.paste('', {row: focus.row - 1, col: this.lines[focus.row - 1].length - 1}, focus);
1032
+ // // Otherwise, we're at the very beginning and can't delete backwards
1033
+ // }
1034
+ // }
1035
+
1036
+ // }
1037
+
1038
+ /**
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
+ *
1041
+ * @param {boolean} getAnchor if set to true, gets the selection anchor (start point of the selection), otherwise gets the focus (end point).
1042
+ * @return {object} An object representing the selection, with properties col and row.
1043
+ */
1044
+ getSelection() {
1045
+ let getAnchor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
1046
+ const selection = window.getSelection();
1047
+ let startNode = getAnchor ? selection.anchorNode : selection.focusNode;
1048
+ if (!startNode) return null;
1049
+ let offset = startNode.nodeType === Node.TEXT_NODE ? getAnchor ? selection.anchorOffset : selection.focusOffset : 0;
1050
+ if (startNode == this.e) {
1384
1051
  return {
1385
- row: row,
1386
- col: col,
1387
- node: startNode
1052
+ row: 0,
1053
+ col: offset
1388
1054
  };
1389
1055
  }
1390
- /**
1391
- * Computes a column within an editor line from a node and offset within that node.
1392
- * @param {Node} startNode The node
1393
- * @param {int} offset THe selection
1394
- * @returns {int} the column, or null if the node is not inside the editor
1395
- */
1396
-
1397
- }, {
1398
- key: "computeColumn",
1399
- value: function computeColumn(startNode, offset) {
1400
- var node = startNode;
1401
- var col = offset; // First, make sure we're actually in the editor.
1402
-
1403
- while (node && node.parentNode != this.e) {
1404
- node = node.parentNode;
1405
- }
1406
-
1407
- if (node == null) return null;
1408
- node = startNode;
1409
-
1410
- while (node.parentNode != this.e) {
1411
- if (node.previousSibling) {
1412
- node = node.previousSibling;
1413
- col += node.textContent.length;
1414
- } else {
1415
- node = node.parentNode;
1416
- }
1417
- }
1056
+ let col = this.computeColumn(startNode, offset);
1057
+ if (col === null) return null; // We are outside of the editor
1418
1058
 
1419
- return col;
1059
+ // Find the row node
1060
+ let node = startNode;
1061
+ while (node.parentElement != this.e) {
1062
+ node = node.parentElement;
1420
1063
  }
1421
- /**
1422
- * Computes DOM node and offset within that node from a position expressed as row and column.
1423
- * @param {int} row Row (line number)
1424
- * @param {int} col Column
1425
- * @returns An object with two properties: node and offset. offset may be null;
1426
- */
1427
-
1428
- }, {
1429
- key: "computeNodeAndOffset",
1430
- value: function computeNodeAndOffset(row, col) {
1431
- var bindRight = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
1432
-
1433
- if (row >= this.lineElements.length) {
1434
- // Selection past the end of text, set selection to end of text
1435
- row = this.lineElements.length - 1;
1436
- col = this.lines[row].length;
1064
+ let row = 0;
1065
+ // Check if we can read a line number from the data-line-num attribute.
1066
+ // The last condition is a security measure since inserting a new paragraph copies the previous rows' line number
1067
+ if (node.dataset && node.dataset.lineNum && (!node.previousSibling || node.previousSibling.dataset.lineNum != node.dataset.lineNum)) {
1068
+ row = parseInt(node.dataset.lineNum);
1069
+ } else {
1070
+ while (node.previousSibling) {
1071
+ row++;
1072
+ node = node.previousSibling;
1437
1073
  }
1074
+ }
1075
+ return {
1076
+ row: row,
1077
+ col: col,
1078
+ node: startNode
1079
+ };
1080
+ }
1438
1081
 
1439
- if (col > this.lines[row].length) {
1440
- col = this.lines[row].length;
1082
+ /**
1083
+ * Computes a column within an editor line from a node and offset within that node.
1084
+ * @param {Node} startNode The node
1085
+ * @param {int} offset THe selection
1086
+ * @returns {int} the column, or null if the node is not inside the editor
1087
+ */
1088
+ computeColumn(startNode, offset) {
1089
+ let node = startNode;
1090
+ let col = offset;
1091
+ // First, make sure we're actually in the editor.
1092
+ while (node && node.parentNode != this.e) {
1093
+ node = node.parentNode;
1094
+ }
1095
+ if (node == null) return null;
1096
+ node = startNode;
1097
+ while (node.parentNode != this.e) {
1098
+ if (node.previousSibling) {
1099
+ node = node.previousSibling;
1100
+ col += node.textContent.length;
1101
+ } else {
1102
+ node = node.parentNode;
1441
1103
  }
1104
+ }
1105
+ return col;
1106
+ }
1442
1107
 
1443
- var parentNode = this.lineElements[row];
1444
- var node = parentNode.firstChild;
1445
- var childrenComplete = false; // default return value
1446
-
1447
- var rv = {
1448
- node: parentNode.firstChild ? parentNode.firstChild : parentNode,
1449
- offset: 0
1450
- };
1451
-
1452
- while (node != parentNode) {
1453
- if (!childrenComplete && node.nodeType === Node.TEXT_NODE) {
1454
- if (node.nodeValue.length >= col) {
1455
- if (bindRight && node.nodeValue.length == col) {
1456
- // Selection is at the end of this text node, but we are binding right (prefer an offset of 0 in the next text node)
1457
- // Remember return value in case we don't find another text node
1458
- rv = {
1459
- node: node,
1460
- offset: col
1461
- };
1462
- col = 0;
1463
- } else {
1464
- return {
1465
- node: node,
1466
- offset: col
1467
- };
1468
- }
1108
+ /**
1109
+ * Computes DOM node and offset within that node from a position expressed as row and column.
1110
+ * @param {int} row Row (line number)
1111
+ * @param {int} col Column
1112
+ * @returns An object with two properties: node and offset. offset may be null;
1113
+ */
1114
+ computeNodeAndOffset(row, col) {
1115
+ let bindRight = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
1116
+ if (row >= this.lineElements.length) {
1117
+ // Selection past the end of text, set selection to end of text
1118
+ row = this.lineElements.length - 1;
1119
+ col = this.lines[row].length;
1120
+ }
1121
+ if (col > this.lines[row].length) {
1122
+ col = this.lines[row].length;
1123
+ }
1124
+ const parentNode = this.lineElements[row];
1125
+ let node = parentNode.firstChild;
1126
+ let childrenComplete = false;
1127
+ // default return value
1128
+ let rv = {
1129
+ node: parentNode.firstChild ? parentNode.firstChild : parentNode,
1130
+ offset: 0
1131
+ };
1132
+ while (node != parentNode) {
1133
+ if (!childrenComplete && node.nodeType === Node.TEXT_NODE) {
1134
+ if (node.nodeValue.length >= col) {
1135
+ if (bindRight && node.nodeValue.length == col) {
1136
+ // Selection is at the end of this text node, but we are binding right (prefer an offset of 0 in the next text node)
1137
+ // Remember return value in case we don't find another text node
1138
+ rv = {
1139
+ node: node,
1140
+ offset: col
1141
+ };
1142
+ col = 0;
1469
1143
  } else {
1470
- col -= node.nodeValue.length;
1144
+ return {
1145
+ node: node,
1146
+ offset: col
1147
+ };
1471
1148
  }
1472
- }
1473
-
1474
- if (!childrenComplete && node.firstChild) {
1475
- node = node.firstChild;
1476
- } else if (node.nextSibling) {
1477
- childrenComplete = false;
1478
- node = node.nextSibling;
1479
1149
  } else {
1480
- childrenComplete = true;
1481
- node = node.parentNode;
1150
+ col -= node.nodeValue.length;
1482
1151
  }
1483
- } // Either, the position was invalid and we just return the default return value
1484
- // Or we are binding right and the selection is at the end of the line
1485
-
1486
-
1487
- return rv;
1152
+ }
1153
+ if (!childrenComplete && node.firstChild) {
1154
+ node = node.firstChild;
1155
+ } else if (node.nextSibling) {
1156
+ childrenComplete = false;
1157
+ node = node.nextSibling;
1158
+ } else {
1159
+ childrenComplete = true;
1160
+ node = node.parentNode;
1161
+ }
1488
1162
  }
1489
- /**
1490
- * Sets the selection based on rows and columns within the editor Markdown content.
1491
- * @param {object} focus Object representing the selection, needs to have properties row and col.
1492
- */
1493
-
1494
- }, {
1495
- key: "setSelection",
1496
- value: function setSelection(focus) {
1497
- var anchor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
1498
- if (!focus) return;
1499
- var range = document.createRange();
1500
1163
 
1501
- var _this$computeNodeAndO = this.computeNodeAndOffset(focus.row, focus.col, anchor && anchor.row == focus.row && anchor.col > focus.col),
1502
- focusNode = _this$computeNodeAndO.node,
1503
- focusOffset = _this$computeNodeAndO.offset; // Bind selection right if anchor is in the same row and behind the focus
1504
-
1505
-
1506
- var anchorNode = null,
1507
- anchorOffset = null;
1508
-
1509
- if (anchor && (anchor.row != focus.row || anchor.col != focus.col)) {
1510
- var _this$computeNodeAndO2 = this.computeNodeAndOffset(anchor.row, anchor.col, focus.row == anchor.row && focus.col > anchor.col),
1511
- node = _this$computeNodeAndO2.node,
1512
- offset = _this$computeNodeAndO2.offset;
1513
-
1514
- anchorNode = node;
1515
- anchorOffset = offset;
1516
- }
1164
+ // Either, the position was invalid and we just return the default return value
1165
+ // Or we are binding right and the selection is at the end of the line
1166
+ return rv;
1167
+ }
1517
1168
 
1518
- if (anchorNode) range.setStart(anchorNode, anchorOffset);else range.setStart(focusNode, focusOffset);
1519
- range.setEnd(focusNode, focusOffset);
1520
- var windowSelection = window.getSelection();
1521
- windowSelection.removeAllRanges();
1522
- windowSelection.addRange(range);
1169
+ /**
1170
+ * Sets the selection based on rows and columns within the editor Markdown content.
1171
+ * @param {object} focus Object representing the selection, needs to have properties row and col.
1172
+ */
1173
+ setSelection(focus) {
1174
+ let anchor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
1175
+ if (!focus) return;
1176
+ let range = document.createRange();
1177
+ let {
1178
+ node: focusNode,
1179
+ offset: focusOffset
1180
+ } = this.computeNodeAndOffset(focus.row, focus.col, anchor && anchor.row == focus.row && anchor.col > focus.col); // Bind selection right if anchor is in the same row and behind the focus
1181
+ let anchorNode = null,
1182
+ anchorOffset = null;
1183
+ if (anchor && (anchor.row != focus.row || anchor.col != focus.col)) {
1184
+ let {
1185
+ node,
1186
+ offset
1187
+ } = this.computeNodeAndOffset(anchor.row, anchor.col, focus.row == anchor.row && focus.col > anchor.col);
1188
+ anchorNode = node;
1189
+ anchorOffset = offset;
1523
1190
  }
1524
- /**
1525
- * Event handler for input events
1526
- */
1527
-
1528
- }, {
1529
- key: "handleInputEvent",
1530
- value: function handleInputEvent(event) {
1531
- var focus = this.getSelection();
1532
-
1533
- if ((event.inputType == 'insertParagraph' || event.inputType == 'insertLineBreak') && focus) {
1534
- this.clearDirtyFlag();
1535
- this.processNewParagraph(focus);
1191
+ if (anchorNode) range.setStart(anchorNode, anchorOffset);else range.setStart(focusNode, focusOffset);
1192
+ range.setEnd(focusNode, focusOffset);
1193
+ let windowSelection = window.getSelection();
1194
+ windowSelection.removeAllRanges();
1195
+ windowSelection.addRange(range);
1196
+ }
1197
+
1198
+ /**
1199
+ * Event handler for input events
1200
+ */
1201
+ handleInputEvent(event) {
1202
+ let focus = this.getSelection();
1203
+ if ((event.inputType == 'insertParagraph' || event.inputType == 'insertLineBreak') && focus) {
1204
+ this.clearDirtyFlag();
1205
+ this.processNewParagraph(focus);
1206
+ } else {
1207
+ if (!this.e.firstChild) {
1208
+ this.e.innerHTML = '<div class="TMBlankLine"><br></div>';
1536
1209
  } else {
1537
- if (!this.e.firstChild) {
1538
- this.e.innerHTML = '<div class="TMBlankLine"><br></div>';
1539
- } else {
1540
- for (var childNode = this.e.firstChild; childNode; childNode = childNode.nextSibling) {
1541
- if (childNode.nodeType != 3 || childNode.tagName != 'DIV') {
1542
- // Found a child node that's either not an element or not a div. Wrap it in a div.
1543
- var divWrapper = document.createElement('div');
1544
- this.e.insertBefore(divWrapper, childNode);
1545
- this.e.removeChild(childNode);
1546
- divWrapper.appendChild(childNode);
1547
- }
1210
+ for (let childNode = this.e.firstChild; childNode; childNode = childNode.nextSibling) {
1211
+ if (childNode.nodeType != 3 || childNode.tagName != 'DIV') {
1212
+ // Found a child node that's either not an element or not a div. Wrap it in a div.
1213
+ let divWrapper = document.createElement('div');
1214
+ this.e.insertBefore(divWrapper, childNode);
1215
+ this.e.removeChild(childNode);
1216
+ divWrapper.appendChild(childNode);
1548
1217
  }
1549
1218
  }
1550
-
1551
- this.updateLineContentsAndFormatting();
1552
1219
  }
1553
-
1554
- if (focus) this.setSelection(focus);
1555
- this.fireChange();
1220
+ this.updateLineContentsAndFormatting();
1556
1221
  }
1557
- /**
1558
- * Event handler for "selectionchange" events.
1559
- */
1560
-
1561
- }, {
1562
- key: "handleSelectionChangeEvent",
1563
- value: function handleSelectionChangeEvent() {
1564
- this.fireSelection();
1565
- }
1566
- /**
1567
- * Convenience function to "splice" new lines into the arrays this.lines, this.lineDirty, this.lineTypes, and the DOM elements
1568
- * underneath the editor element.
1569
- * This method is essentially Array.splice, only that the third parameter takes an un-spread array (and the forth parameter)
1570
- * determines whether the DOM should also be adjusted.
1571
- *
1572
- * @param {int} startLine Position at which to start changing the array of lines
1573
- * @param {int} linesToDelete Number of lines to delete
1574
- * @param {array} linesToInsert Array of strings representing the lines to be inserted
1575
- * @param {boolean} adjustLineElements If true, then <div> elements are also inserted in the DOM at the respective position
1576
- */
1577
-
1578
- }, {
1579
- key: "spliceLines",
1580
- value: function spliceLines(startLine) {
1581
- var _this$lines, _this$lineTypes, _this$lineDirty;
1582
-
1583
- var linesToDelete = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
1584
- var linesToInsert = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
1585
- var adjustLineElements = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
1222
+ if (focus) this.setSelection(focus);
1223
+ this.fireChange();
1224
+ }
1225
+
1226
+ /**
1227
+ * Event handler for "selectionchange" events.
1228
+ */
1229
+ handleSelectionChangeEvent() {
1230
+ this.fireSelection();
1231
+ }
1586
1232
 
1233
+ /**
1234
+ * Convenience function to "splice" new lines into the arrays this.lines, this.lineDirty, this.lineTypes, and the DOM elements
1235
+ * underneath the editor element.
1236
+ * This method is essentially Array.splice, only that the third parameter takes an un-spread array (and the forth parameter)
1237
+ * determines whether the DOM should also be adjusted.
1238
+ *
1239
+ * @param {int} startLine Position at which to start changing the array of lines
1240
+ * @param {int} linesToDelete Number of lines to delete
1241
+ * @param {array} linesToInsert Array of strings representing the lines to be inserted
1242
+ * @param {boolean} adjustLineElements If true, then <div> elements are also inserted in the DOM at the respective position
1243
+ */
1244
+ spliceLines(startLine) {
1245
+ let linesToDelete = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
1246
+ let linesToInsert = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
1247
+ let adjustLineElements = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
1248
+ if (adjustLineElements) {
1249
+ for (let i = 0; i < linesToDelete; i++) {
1250
+ this.e.removeChild(this.e.childNodes[startLine]);
1251
+ }
1252
+ }
1253
+ let insertedBlank = [];
1254
+ let insertedDirty = [];
1255
+ for (let i = 0; i < linesToInsert.length; i++) {
1256
+ insertedBlank.push('');
1257
+ insertedDirty.push(true);
1587
1258
  if (adjustLineElements) {
1588
- for (var i = 0; i < linesToDelete; i++) {
1589
- this.e.removeChild(this.e.childNodes[startLine]);
1590
- }
1259
+ if (this.e.childNodes[startLine]) this.e.insertBefore(document.createElement('div'), this.e.childNodes[startLine]);else this.e.appendChild(document.createElement('div'));
1591
1260
  }
1261
+ }
1262
+ this.lines.splice(startLine, linesToDelete, ...linesToInsert);
1263
+ this.lineTypes.splice(startLine, linesToDelete, ...insertedBlank);
1264
+ this.lineDirty.splice(startLine, linesToDelete, ...insertedDirty);
1265
+ }
1592
1266
 
1593
- var insertedBlank = [];
1594
- var insertedDirty = [];
1595
-
1596
- for (var _i3 = 0; _i3 < linesToInsert.length; _i3++) {
1597
- insertedBlank.push('');
1598
- insertedDirty.push(true);
1599
-
1600
- if (adjustLineElements) {
1601
- if (this.e.childNodes[startLine]) this.e.insertBefore(document.createElement('div'), this.e.childNodes[startLine]);else this.e.appendChild(document.createElement('div'));
1602
- }
1603
- }
1267
+ /**
1268
+ * Event handler for the "paste" event
1269
+ */
1270
+ handlePaste(event) {
1271
+ event.preventDefault();
1604
1272
 
1605
- (_this$lines = this.lines).splice.apply(_this$lines, [startLine, linesToDelete].concat(_toConsumableArray(linesToInsert)));
1273
+ // get text representation of clipboard
1274
+ let text = (event.originalEvent || event).clipboardData.getData('text/plain');
1606
1275
 
1607
- (_this$lineTypes = this.lineTypes).splice.apply(_this$lineTypes, [startLine, linesToDelete].concat(insertedBlank));
1276
+ // insert text manually
1277
+ this.paste(text);
1278
+ }
1608
1279
 
1609
- (_this$lineDirty = this.lineDirty).splice.apply(_this$lineDirty, [startLine, linesToDelete].concat(insertedDirty));
1280
+ /**
1281
+ * Pastes the text at the current selection (or at the end, if no current selection)
1282
+ * @param {string} text
1283
+ */
1284
+ paste(text) {
1285
+ let anchor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
1286
+ let focus = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
1287
+ if (!anchor) anchor = this.getSelection(true);
1288
+ if (!focus) focus = this.getSelection(false);
1289
+ let beginning, end;
1290
+ if (!focus) {
1291
+ focus = {
1292
+ row: this.lines.length - 1,
1293
+ col: this.lines[this.lines.length - 1].length
1294
+ }; // Insert at end
1610
1295
  }
1611
- /**
1612
- * Event handler for the "paste" event
1613
- */
1614
-
1615
- }, {
1616
- key: "handlePaste",
1617
- value: function handlePaste(event) {
1618
- event.preventDefault(); // get text representation of clipboard
1619
1296
 
1620
- var text = (event.originalEvent || event).clipboardData.getData('text/plain'); // insert text manually
1621
-
1622
- this.paste(text);
1297
+ if (!anchor) {
1298
+ anchor = focus;
1623
1299
  }
1624
- /**
1625
- * Pastes the text at the current selection (or at the end, if no current selection)
1626
- * @param {string} text
1627
- */
1628
-
1629
- }, {
1630
- key: "paste",
1631
- value: function paste(text) {
1632
- var anchor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
1633
- var focus = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
1634
- if (!anchor) anchor = this.getSelection(true);
1635
- if (!focus) focus = this.getSelection(false);
1636
- var beginning, end;
1637
-
1638
- if (!focus) {
1639
- focus = {
1640
- row: this.lines.length - 1,
1641
- col: this.lines[this.lines.length - 1].length
1642
- }; // Insert at end
1643
- }
1644
-
1645
- if (!anchor) {
1646
- anchor = focus;
1647
- }
1300
+ if (anchor.row < focus.row || anchor.row == focus.row && anchor.col <= focus.col) {
1301
+ beginning = anchor;
1302
+ end = focus;
1303
+ } else {
1304
+ beginning = focus;
1305
+ end = anchor;
1306
+ }
1307
+ let insertedLines = text.split(/(?:\r\n|\r|\n)/);
1308
+ let lineBefore = this.lines[beginning.row].substr(0, beginning.col);
1309
+ let lineEnd = this.lines[end.row].substr(end.col);
1310
+ insertedLines[0] = lineBefore.concat(insertedLines[0]);
1311
+ let endColPos = insertedLines[insertedLines.length - 1].length;
1312
+ insertedLines[insertedLines.length - 1] = insertedLines[insertedLines.length - 1].concat(lineEnd);
1313
+ this.spliceLines(beginning.row, 1 + end.row - beginning.row, insertedLines);
1314
+ focus.row = beginning.row + insertedLines.length - 1;
1315
+ focus.col = endColPos;
1316
+ this.updateFormatting();
1317
+ this.setSelection(focus);
1318
+ this.fireChange();
1319
+ }
1648
1320
 
1649
- if (anchor.row < focus.row || anchor.row == focus.row && anchor.col <= focus.col) {
1650
- beginning = anchor;
1651
- end = focus;
1652
- } else {
1653
- beginning = focus;
1654
- end = anchor;
1321
+ /**
1322
+ * Computes the (lowest in the DOM tree) common ancestor of two DOM nodes.
1323
+ * @param {Node} node1 the first node
1324
+ * @param {Node} node2 the second node
1325
+ * @returns {Node} The commen ancestor node, or null if there is no common ancestor
1326
+ */
1327
+ computeCommonAncestor(node1, node2) {
1328
+ if (!node1 || !node2) return null;
1329
+ if (node1 == node2) return node1;
1330
+ const ancestry = node => {
1331
+ let ancestry = [];
1332
+ while (node) {
1333
+ ancestry.unshift(node);
1334
+ node = node.parentNode;
1655
1335
  }
1336
+ return ancestry;
1337
+ };
1338
+ const ancestry1 = ancestry(node1);
1339
+ const ancestry2 = ancestry(node2);
1340
+ if (ancestry1[0] != ancestry2[0]) return null;
1341
+ let i;
1342
+ for (i = 0; ancestry1[i] == ancestry2[i]; i++);
1343
+ return ancestry1[i - 1];
1344
+ }
1656
1345
 
1657
- var insertedLines = text.split(/(?:\r\n|\r|\n)/);
1658
- var lineBefore = this.lines[beginning.row].substr(0, beginning.col);
1659
- var lineEnd = this.lines[end.row].substr(end.col);
1660
- insertedLines[0] = lineBefore.concat(insertedLines[0]);
1661
- var endColPos = insertedLines[insertedLines.length - 1].length;
1662
- insertedLines[insertedLines.length - 1] = insertedLines[insertedLines.length - 1].concat(lineEnd);
1663
- this.spliceLines(beginning.row, 1 + end.row - beginning.row, insertedLines);
1664
- focus.row = beginning.row + insertedLines.length - 1;
1665
- focus.col = endColPos;
1666
- this.updateFormatting();
1667
- this.setSelection(focus);
1668
- this.fireChange();
1346
+ /**
1347
+ * Finds the (lowest in the DOM tree) enclosing DOM node with a given class.
1348
+ * @param {object} focus The focus selection object
1349
+ * @param {object} anchor The anchor selection object
1350
+ * @param {string} className The class name to find
1351
+ * @returns {Node} The enclosing DOM node with the respective class (inside the editor), if there is one; null otherwise.
1352
+ */
1353
+ computeEnclosingMarkupNode(focus, anchor, className) {
1354
+ let node = null;
1355
+ if (!focus) return null;
1356
+ if (!anchor) {
1357
+ node = focus.node;
1358
+ } else {
1359
+ if (focus.row != anchor.row) return null;
1360
+ node = this.computeCommonAncestor(focus.node, anchor.node);
1669
1361
  }
1670
- /**
1671
- * Computes the (lowest in the DOM tree) common ancestor of two DOM nodes.
1672
- * @param {Node} node1 the first node
1673
- * @param {Node} node2 the second node
1674
- * @returns {Node} The commen ancestor node, or null if there is no common ancestor
1675
- */
1676
-
1677
- }, {
1678
- key: "computeCommonAncestor",
1679
- value: function computeCommonAncestor(node1, node2) {
1680
- if (!node1 || !node2) return null;
1681
- if (node1 == node2) return node1;
1682
-
1683
- var ancestry = function ancestry(node) {
1684
- var ancestry = [];
1685
-
1686
- while (node) {
1687
- ancestry.unshift(node);
1688
- node = node.parentNode;
1689
- }
1690
-
1691
- return ancestry;
1692
- };
1693
-
1694
- var ancestry1 = ancestry(node1);
1695
- var ancestry2 = ancestry(node2);
1696
- if (ancestry1[0] != ancestry2[0]) return null;
1697
- var i;
1362
+ if (!node) return null;
1363
+ while (node != this.e) {
1364
+ if (node.className && node.className.includes(className)) return node;
1365
+ node = node.parentNode;
1366
+ }
1367
+ // Ascended all the way to the editor element
1368
+ return null;
1369
+ }
1698
1370
 
1699
- for (i = 0; ancestry1[i] == ancestry2[i]; i++) {
1700
- ;
1371
+ /**
1372
+ * Returns the state (true / false) of all commands.
1373
+ * @param focus Focus of the selection. If not given, assumes the current focus.
1374
+ */
1375
+ getCommandState() {
1376
+ let focus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
1377
+ let anchor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
1378
+ let commandState = {};
1379
+ if (!focus) focus = this.getSelection(false);
1380
+ if (!anchor) anchor = this.getSelection(true);
1381
+ if (!focus) {
1382
+ for (let cmd in _grammar.commands) {
1383
+ commandState[cmd] = null;
1701
1384
  }
1702
-
1703
- return ancestry1[i - 1];
1385
+ return commandState;
1386
+ }
1387
+ if (!anchor) anchor = focus;
1388
+ let start, end;
1389
+ if (anchor.row < focus.row || anchor.row == focus.row && anchor.col < focus.col) {
1390
+ start = anchor;
1391
+ end = focus;
1392
+ } else {
1393
+ start = focus;
1394
+ end = anchor;
1395
+ }
1396
+ if (end.row > start.row && end.col == 0) {
1397
+ end.row--;
1398
+ end.col = this.lines[end.row].length; // Selection to beginning of next line is said to end at the beginning of the last line
1704
1399
  }
1705
- /**
1706
- * Finds the (lowest in the DOM tree) enclosing DOM node with a given class.
1707
- * @param {object} focus The focus selection object
1708
- * @param {object} anchor The anchor selection object
1709
- * @param {string} className The class name to find
1710
- * @returns {Node} The enclosing DOM node with the respective class (inside the editor), if there is one; null otherwise.
1711
- */
1712
-
1713
- }, {
1714
- key: "computeEnclosingMarkupNode",
1715
- value: function computeEnclosingMarkupNode(focus, anchor, className) {
1716
- var node = null;
1717
- if (!focus) return null;
1718
-
1719
- if (!anchor) {
1720
- node = focus.node;
1721
- } else {
1722
- if (focus.row != anchor.row) return null;
1723
- node = this.computeCommonAncestor(focus.node, anchor.node);
1724
- }
1725
-
1726
- if (!node) return null;
1727
-
1728
- while (node != this.e) {
1729
- if (node.className && node.className.includes(className)) return node;
1730
- node = node.parentNode;
1731
- } // Ascended all the way to the editor element
1732
1400
 
1401
+ for (let cmd in _grammar.commands) {
1402
+ if (_grammar.commands[cmd].type == 'inline') {
1403
+ if (!focus || focus.row != anchor.row || !this.isInlineFormattingAllowed(focus, anchor)) {
1404
+ commandState[cmd] = null;
1405
+ } else {
1406
+ // The command state is true if there is a respective enclosing markup node (e.g., the selection is enclosed in a <b>..</b>) ...
1407
+ commandState[cmd] = !!this.computeEnclosingMarkupNode(focus, anchor, _grammar.commands[cmd].className) ||
1408
+ // ... or if it's an empty string preceded by and followed by formatting markers, e.g. **|** where | is the cursor
1733
1409
 
1734
- return null;
1735
- }
1736
- /**
1737
- * Returns the state (true / false) of all commands.
1738
- * @param focus Focus of the selection. If not given, assumes the current focus.
1739
- */
1740
-
1741
- }, {
1742
- key: "getCommandState",
1743
- value: function getCommandState() {
1744
- var focus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
1745
- var anchor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
1746
- var commandState = {};
1747
- if (!focus) focus = this.getSelection(false);
1748
- if (!anchor) anchor = this.getSelection(true);
1749
-
1750
- if (!focus) {
1751
- for (var cmd in _grammar.commands) {
1410
+ 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);
1411
+ }
1412
+ }
1413
+ if (_grammar.commands[cmd].type == 'line') {
1414
+ if (!focus) {
1752
1415
  commandState[cmd] = null;
1416
+ } else {
1417
+ let state = this.lineTypes[start.row] == _grammar.commands[cmd].className;
1418
+ for (let line = start.row; line <= end.row; line++) {
1419
+ if (this.lineTypes[line] == _grammar.commands[cmd].className != state) {
1420
+ state = null;
1421
+ break;
1422
+ }
1423
+ }
1424
+ commandState[cmd] = state;
1753
1425
  }
1754
-
1755
- return commandState;
1756
1426
  }
1427
+ }
1428
+ return commandState;
1429
+ }
1757
1430
 
1431
+ /**
1432
+ * Sets a command state
1433
+ * @param {string} command
1434
+ * @param {boolean} state
1435
+ */
1436
+ setCommandState(command, state) {
1437
+ if (_grammar.commands[command].type == 'inline') {
1438
+ let anchor = this.getSelection(true);
1439
+ let focus = this.getSelection(false);
1758
1440
  if (!anchor) anchor = focus;
1759
- var start, end;
1441
+ if (!anchor) return;
1442
+ if (anchor.row != focus.row) return;
1443
+ if (!this.isInlineFormattingAllowed(focus, anchor)) return;
1444
+ let markupNode = this.computeEnclosingMarkupNode(focus, anchor, _grammar.commands[command].className);
1445
+ this.clearDirtyFlag();
1446
+
1447
+ // First case: There's an enclosing markup node, remove the markers around that markup node
1448
+ if (markupNode) {
1449
+ this.lineDirty[focus.row] = true;
1450
+ const startCol = this.computeColumn(markupNode, 0);
1451
+ const len = markupNode.textContent.length;
1452
+ const left = this.lines[focus.row].substr(0, startCol).replace(_grammar.commands[command].unset.prePattern, '');
1453
+ const mid = this.lines[focus.row].substr(startCol, len);
1454
+ const right = this.lines[focus.row].substr(startCol + len).replace(_grammar.commands[command].unset.postPattern, '');
1455
+ this.lines[focus.row] = left.concat(mid, right);
1456
+ anchor.col = left.length;
1457
+ focus.col = anchor.col + len;
1458
+ this.updateFormatting();
1459
+ this.setSelection(focus, anchor);
1460
+
1461
+ // Second case: Empty selection with surrounding formatting markers, remove those
1462
+ } 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)) {
1463
+ this.lineDirty[focus.row] = true;
1464
+ const left = this.lines[focus.row].substr(0, focus.col).replace(_grammar.commands[command].unset.prePattern, '');
1465
+ const right = this.lines[focus.row].substr(focus.col).replace(_grammar.commands[command].unset.postPattern, '');
1466
+ this.lines[focus.row] = left.concat(right);
1467
+ focus.col = anchor.col = left.length;
1468
+ this.updateFormatting();
1469
+ this.setSelection(focus, anchor);
1760
1470
 
1761
- if (anchor.row < focus.row || anchor.row == focus.row && anchor.col < focus.col) {
1762
- start = anchor;
1763
- end = focus;
1471
+ // Not currently formatted, insert formatting markers
1764
1472
  } else {
1765
- start = focus;
1766
- end = anchor;
1767
- }
1473
+ // Trim any spaces from the selection
1474
+ let {
1475
+ startCol,
1476
+ endCol
1477
+ } = focus.col < anchor.col ? {
1478
+ startCol: focus.col,
1479
+ endCol: anchor.col
1480
+ } : {
1481
+ startCol: anchor.col,
1482
+ endCol: focus.col
1483
+ };
1484
+ let match = this.lines[focus.row].substr(startCol, endCol - startCol).match(/^(?<leading>\s*).*\S(?<trailing>\s*)$/);
1485
+ if (match) {
1486
+ startCol += match.groups.leading.length;
1487
+ endCol -= match.groups.trailing.length;
1488
+ }
1489
+ focus.col = startCol;
1490
+ anchor.col = endCol;
1768
1491
 
1492
+ // Just insert markup before and after and hope for the best.
1493
+ this.wrapSelection(_grammar.commands[command].set.pre, _grammar.commands[command].set.post, focus, anchor);
1494
+ // TODO clean this up so that markup remains properly nested
1495
+ }
1496
+ } else if (_grammar.commands[command].type == 'line') {
1497
+ let anchor = this.getSelection(true);
1498
+ let focus = this.getSelection(false);
1499
+ if (!anchor) anchor = focus;
1500
+ if (!focus) return;
1501
+ this.clearDirtyFlag();
1502
+ let start = anchor.row > focus.row ? focus : anchor;
1503
+ let end = anchor.row > focus.row ? anchor : focus;
1769
1504
  if (end.row > start.row && end.col == 0) {
1770
1505
  end.row--;
1771
- end.col = this.lines[end.row].length; // Selection to beginning of next line is said to end at the beginning of the last line
1772
1506
  }
1773
-
1774
- for (var _cmd in _grammar.commands) {
1775
- if (_grammar.commands[_cmd].type == 'inline') {
1776
- if (!focus || focus.row != anchor.row || !this.isInlineFormattingAllowed(focus, anchor)) {
1777
- commandState[_cmd] = null;
1778
- } else {
1779
- // The command state is true if there is a respective enclosing markup node (e.g., the selection is enclosed in a <b>..</b>) ...
1780
- commandState[_cmd] = !!this.computeEnclosingMarkupNode(focus, anchor, _grammar.commands[_cmd].className) || // ... or if it's an empty string preceded by and followed by formatting markers, e.g. **|** where | is the cursor
1781
- 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);
1782
- }
1507
+ for (let line = start.row; line <= end.row; line++) {
1508
+ if (state && this.lineTypes[line] != _grammar.commands[command].className) {
1509
+ this.lines[line] = this.lines[line].replace(_grammar.commands[command].set.pattern, _grammar.commands[command].set.replacement.replace('$#', line - start.row + 1));
1510
+ this.lineDirty[line] = true;
1783
1511
  }
1784
-
1785
- if (_grammar.commands[_cmd].type == 'line') {
1786
- if (!focus) {
1787
- commandState[_cmd] = null;
1788
- } else {
1789
- var state = this.lineTypes[start.row] == _grammar.commands[_cmd].className;
1790
-
1791
- for (var line = start.row; line <= end.row; line++) {
1792
- if (this.lineTypes[line] == _grammar.commands[_cmd].className != state) {
1793
- state = null;
1794
- break;
1795
- }
1796
- }
1797
-
1798
- commandState[_cmd] = state;
1799
- }
1512
+ if (!state && this.lineTypes[line] == _grammar.commands[command].className) {
1513
+ this.lines[line] = this.lines[line].replace(_grammar.commands[command].unset.pattern, _grammar.commands[command].unset.replacement);
1514
+ this.lineDirty[line] = true;
1800
1515
  }
1801
1516
  }
1802
-
1803
- return commandState;
1517
+ this.updateFormatting();
1518
+ this.setSelection({
1519
+ row: end.row,
1520
+ col: this.lines[end.row].length
1521
+ }, {
1522
+ row: start.row,
1523
+ col: 0
1524
+ });
1804
1525
  }
1805
- /**
1806
- * Sets a command state
1807
- * @param {string} command
1808
- * @param {boolean} state
1809
- */
1810
-
1811
- }, {
1812
- key: "setCommandState",
1813
- value: function setCommandState(command, state) {
1814
- if (_grammar.commands[command].type == 'inline') {
1815
- var anchor = this.getSelection(true);
1816
- var focus = this.getSelection(false);
1817
- if (!anchor) anchor = focus;
1818
- if (!anchor) return;
1819
- if (anchor.row != focus.row) return;
1820
- if (!this.isInlineFormattingAllowed(focus, anchor)) return;
1821
- var markupNode = this.computeEnclosingMarkupNode(focus, anchor, _grammar.commands[command].className);
1822
- this.clearDirtyFlag(); // First case: There's an enclosing markup node, remove the markers around that markup node
1823
-
1824
- if (markupNode) {
1825
- this.lineDirty[focus.row] = true;
1826
- var startCol = this.computeColumn(markupNode, 0);
1827
- var len = markupNode.textContent.length;
1828
- var left = this.lines[focus.row].substr(0, startCol).replace(_grammar.commands[command].unset.prePattern, '');
1829
- var mid = this.lines[focus.row].substr(startCol, len);
1830
- var right = this.lines[focus.row].substr(startCol + len).replace(_grammar.commands[command].unset.postPattern, '');
1831
- this.lines[focus.row] = left.concat(mid, right);
1832
- anchor.col = left.length;
1833
- focus.col = anchor.col + len;
1834
- this.updateFormatting();
1835
- this.setSelection(focus, anchor); // Second case: Empty selection with surrounding formatting markers, remove those
1836
- } 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)) {
1837
- this.lineDirty[focus.row] = true;
1838
-
1839
- var _left = this.lines[focus.row].substr(0, focus.col).replace(_grammar.commands[command].unset.prePattern, '');
1840
-
1841
- var _right = this.lines[focus.row].substr(focus.col).replace(_grammar.commands[command].unset.postPattern, '');
1842
-
1843
- this.lines[focus.row] = _left.concat(_right);
1844
- focus.col = anchor.col = _left.length;
1845
- this.updateFormatting();
1846
- this.setSelection(focus, anchor); // Not currently formatted, insert formatting markers
1847
- } else {
1848
- // Trim any spaces from the selection
1849
- var _ref = focus.col < anchor.col ? {
1850
- startCol: focus.col,
1851
- endCol: anchor.col
1852
- } : {
1853
- startCol: anchor.col,
1854
- endCol: focus.col
1855
- },
1856
- _startCol = _ref.startCol,
1857
- endCol = _ref.endCol;
1858
-
1859
- var match = this.lines[focus.row].substr(_startCol, endCol - _startCol).match( /*#__PURE__*/_wrapRegExp(/^([\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*).*[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uFEFE\uFF00-\uFFFF]([\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*)$/, {
1860
- leading: 1,
1861
- trailing: 2
1862
- }));
1863
-
1864
- if (match) {
1865
- _startCol += match.groups.leading.length;
1866
- endCol -= match.groups.trailing.length;
1867
- }
1868
-
1869
- focus.col = _startCol;
1870
- anchor.col = endCol; // Just insert markup before and after and hope for the best.
1871
-
1872
- this.wrapSelection(_grammar.commands[command].set.pre, _grammar.commands[command].set.post, focus, anchor); // TODO clean this up so that markup remains properly nested
1873
- }
1874
- } else if (_grammar.commands[command].type == 'line') {
1875
- var _anchor = this.getSelection(true);
1876
-
1877
- var _focus = this.getSelection(false);
1878
-
1879
- if (!_anchor) _anchor = _focus;
1880
- if (!_focus) return;
1881
- this.clearDirtyFlag();
1882
- var start = _anchor.row > _focus.row ? _focus : _anchor;
1883
- var end = _anchor.row > _focus.row ? _anchor : _focus;
1884
-
1885
- if (end.row > start.row && end.col == 0) {
1886
- end.row--;
1887
- }
1526
+ }
1888
1527
 
1889
- for (var line = start.row; line <= end.row; line++) {
1890
- if (state && this.lineTypes[line] != _grammar.commands[command].className) {
1891
- this.lines[line] = this.lines[line].replace(_grammar.commands[command].set.pattern, _grammar.commands[command].set.replacement.replace('$#', line - start.row + 1));
1892
- this.lineDirty[line] = true;
1893
- }
1528
+ /**
1529
+ * Returns whether or not inline formatting is allowed at the current focus
1530
+ * @param {object} focus The current focus
1531
+ */
1532
+ isInlineFormattingAllowed() {
1533
+ // TODO Remove parameters from all calls
1534
+ const sel = window.getSelection();
1535
+ if (!sel) return false;
1536
+
1537
+ // Check if we can find a common ancestor with the class `TMInlineFormatted`
1538
+
1539
+ // Special case: Empty selection right before `TMInlineFormatted`
1540
+ if (sel.isCollapsed && sel.focusNode.nodeType == 3 && sel.focusOffset == sel.focusNode.nodeValue.length) {
1541
+ let node;
1542
+ for (node = sel.focusNode; node && node.nextSibling == null; node = node.parentNode);
1543
+ if (node && node.nextSibling.className && node.nextSibling.className.includes('TMInlineFormatted')) return true;
1544
+ }
1894
1545
 
1895
- if (!state && this.lineTypes[line] == _grammar.commands[command].className) {
1896
- this.lines[line] = this.lines[line].replace(_grammar.commands[command].unset.pattern, _grammar.commands[command].unset.replacement);
1897
- this.lineDirty[line] = true;
1898
- }
1899
- }
1546
+ // Look for a common ancestor
1547
+ let ancestor = this.computeCommonAncestor(sel.focusNode, sel.anchorNode);
1548
+ if (!ancestor) return false;
1900
1549
 
1901
- this.updateFormatting();
1902
- this.setSelection({
1903
- row: end.row,
1904
- col: this.lines[end.row].length
1905
- }, {
1906
- row: start.row,
1907
- col: 0
1908
- });
1909
- }
1550
+ // Check if there's an ancestor of class 'TMInlineFormatted' or 'TMBlankLine'
1551
+ while (ancestor && ancestor != this.e) {
1552
+ if (ancestor.className && (ancestor.className.includes('TMInlineFormatted') || ancestor.className.includes('TMBlankLine'))) return true;
1553
+ ancestor = ancestor.parentNode;
1910
1554
  }
1911
- /**
1912
- * Returns whether or not inline formatting is allowed at the current focus
1913
- * @param {object} focus The current focus
1914
- */
1915
-
1916
- }, {
1917
- key: "isInlineFormattingAllowed",
1918
- value: function isInlineFormattingAllowed() {
1919
- // TODO Remove parameters from all calls
1920
- var sel = window.getSelection();
1921
- if (!sel) return false; // Check if we can find a common ancestor with the class `TMInlineFormatted`
1922
- // Special case: Empty selection right before `TMInlineFormatted`
1923
-
1924
- if (sel.isCollapsed && sel.focusNode.nodeType == 3 && sel.focusOffset == sel.focusNode.nodeValue.length) {
1925
- var node;
1926
-
1927
- for (node = sel.focusNode; node && node.nextSibling == null; node = node.parentNode) {
1928
- ;
1929
- }
1555
+ return false;
1556
+ }
1930
1557
 
1931
- if (node && node.nextSibling.className && node.nextSibling.className.includes('TMInlineFormatted')) return true;
1932
- } // Look for a common ancestor
1558
+ /**
1559
+ * Wraps the current selection in the strings pre and post. If the selection is not on one line, returns.
1560
+ * @param {string} pre The string to insert before the selection.
1561
+ * @param {string} post The string to insert after the selection.
1562
+ * @param {object} focus The current selection focus. If null, selection will be computed.
1563
+ * @param {object} anchor The current selection focus. If null, selection will be computed.
1564
+ */
1565
+ wrapSelection(pre, post) {
1566
+ let focus = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
1567
+ let anchor = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
1568
+ if (!focus) focus = this.getSelection(false);
1569
+ if (!anchor) anchor = this.getSelection(true);
1570
+ if (!focus || !anchor || focus.row != anchor.row) return;
1571
+ this.lineDirty[focus.row] = true;
1572
+ const startCol = focus.col < anchor.col ? focus.col : anchor.col;
1573
+ const endCol = focus.col < anchor.col ? anchor.col : focus.col;
1574
+ const left = this.lines[focus.row].substr(0, startCol).concat(pre);
1575
+ const mid = endCol == startCol ? '' : this.lines[focus.row].substr(startCol, endCol - startCol);
1576
+ const right = post.concat(this.lines[focus.row].substr(endCol));
1577
+ this.lines[focus.row] = left.concat(mid, right);
1578
+ anchor.col = left.length;
1579
+ focus.col = anchor.col + mid.length;
1580
+ this.updateFormatting();
1581
+ this.setSelection(focus, anchor);
1582
+ }
1933
1583
 
1584
+ /**
1585
+ * Toggles the command state for a command (true <-> false)
1586
+ * @param {string} command The editor command
1587
+ */
1588
+ toggleCommandState(command) {
1589
+ if (!this.lastCommandState) this.lastCommandState = this.getCommandState();
1590
+ this.setCommandState(command, !this.lastCommandState[command]);
1591
+ }
1934
1592
 
1935
- var ancestor = this.computeCommonAncestor(sel.focusNode, sel.anchorNode);
1936
- if (!ancestor) return false; // Check if there's an ancestor of class 'TMInlineFormatted' or 'TMBlankLine'
1593
+ /**
1594
+ * Fires a change event. Updates the linked textarea and notifies any event listeners.
1595
+ */
1596
+ fireChange() {
1597
+ if (!this.textarea && !this.listeners.change.length) return;
1598
+ const content = this.getContent();
1599
+ if (this.textarea) this.textarea.value = content;
1600
+ for (let listener of this.listeners.change) {
1601
+ listener({
1602
+ content: content,
1603
+ linesDirty: this.linesDirty
1604
+ });
1605
+ }
1606
+ }
1937
1607
 
1938
- while (ancestor && ancestor != this.e) {
1939
- if (ancestor.className && (ancestor.className.includes('TMInlineFormatted') || ancestor.className.includes('TMBlankLine'))) return true;
1940
- ancestor = ancestor.parentNode;
1608
+ /**
1609
+ * Fires a "selection changed" event.
1610
+ */
1611
+ fireSelection() {
1612
+ if (this.listeners.selection && this.listeners.selection.length) {
1613
+ let focus = this.getSelection(false);
1614
+ let anchor = this.getSelection(true);
1615
+ let commandState = this.getCommandState(focus, anchor);
1616
+ if (this.lastCommandState) {
1617
+ Object.assign(this.lastCommandState, commandState);
1618
+ } else {
1619
+ this.lastCommandState = Object.assign({}, commandState);
1941
1620
  }
1942
-
1943
- return false;
1944
- }
1945
- /**
1946
- * Wraps the current selection in the strings pre and post. If the selection is not on one line, returns.
1947
- * @param {string} pre The string to insert before the selection.
1948
- * @param {string} post The string to insert after the selection.
1949
- * @param {object} focus The current selection focus. If null, selection will be computed.
1950
- * @param {object} anchor The current selection focus. If null, selection will be computed.
1951
- */
1952
-
1953
- }, {
1954
- key: "wrapSelection",
1955
- value: function wrapSelection(pre, post) {
1956
- var focus = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
1957
- var anchor = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
1958
- if (!focus) focus = this.getSelection(false);
1959
- if (!anchor) anchor = this.getSelection(true);
1960
- if (!focus || !anchor || focus.row != anchor.row) return;
1961
- this.lineDirty[focus.row] = true;
1962
- var startCol = focus.col < anchor.col ? focus.col : anchor.col;
1963
- var endCol = focus.col < anchor.col ? anchor.col : focus.col;
1964
- var left = this.lines[focus.row].substr(0, startCol).concat(pre);
1965
- var mid = endCol == startCol ? '' : this.lines[focus.row].substr(startCol, endCol - startCol);
1966
- var right = post.concat(this.lines[focus.row].substr(endCol));
1967
- this.lines[focus.row] = left.concat(mid, right);
1968
- anchor.col = left.length;
1969
- focus.col = anchor.col + mid.length;
1970
- this.updateFormatting();
1971
- this.setSelection(focus, anchor);
1972
- }
1973
- /**
1974
- * Toggles the command state for a command (true <-> false)
1975
- * @param {string} command The editor command
1976
- */
1977
-
1978
- }, {
1979
- key: "toggleCommandState",
1980
- value: function toggleCommandState(command) {
1981
- if (!this.lastCommandState) this.lastCommandState = this.getCommandState();
1982
- this.setCommandState(command, !this.lastCommandState[command]);
1983
- }
1984
- /**
1985
- * Fires a change event. Updates the linked textarea and notifies any event listeners.
1986
- */
1987
-
1988
- }, {
1989
- key: "fireChange",
1990
- value: function fireChange() {
1991
- if (!this.textarea && !this.listeners.change.length) return;
1992
- var content = this.getContent();
1993
- if (this.textarea) this.textarea.value = content;
1994
-
1995
- var _iterator3 = _createForOfIteratorHelper(this.listeners.change),
1996
- _step3;
1997
-
1998
- try {
1999
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
2000
- var listener = _step3.value;
2001
- listener({
2002
- content: content,
2003
- linesDirty: this.linesDirty
2004
- });
2005
- }
2006
- } catch (err) {
2007
- _iterator3.e(err);
2008
- } finally {
2009
- _iterator3.f();
1621
+ for (let listener of this.listeners.selection) {
1622
+ listener({
1623
+ focus: focus,
1624
+ anchor: anchor,
1625
+ commandState: this.lastCommandState
1626
+ });
2010
1627
  }
2011
1628
  }
2012
- /**
2013
- * Fires a "selection changed" event.
2014
- */
2015
-
2016
- }, {
2017
- key: "fireSelection",
2018
- value: function fireSelection() {
2019
- if (this.listeners.selection && this.listeners.selection.length) {
2020
- var focus = this.getSelection(false);
2021
- var anchor = this.getSelection(true);
2022
- var commandState = this.getCommandState(focus, anchor);
2023
-
2024
- if (this.lastCommandState) {
2025
- Object.assign(this.lastCommandState, commandState);
2026
- } else {
2027
- this.lastCommandState = Object.assign({}, commandState);
2028
- }
2029
-
2030
- var _iterator4 = _createForOfIteratorHelper(this.listeners.selection),
2031
- _step4;
1629
+ }
2032
1630
 
2033
- try {
2034
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
2035
- var listener = _step4.value;
2036
- listener({
2037
- focus: focus,
2038
- anchor: anchor,
2039
- commandState: this.lastCommandState
2040
- });
2041
- }
2042
- } catch (err) {
2043
- _iterator4.e(err);
2044
- } finally {
2045
- _iterator4.f();
2046
- }
2047
- }
1631
+ /**
1632
+ * Adds an event listener.
1633
+ * @param {string} type The type of event to listen to. Can be 'change' or 'selection'
1634
+ * @param {*} listener Function of the type (event) => {} to be called when the event occurs.
1635
+ */
1636
+ addEventListener(type, listener) {
1637
+ if (type.match(/^(?:change|input)$/i)) {
1638
+ this.listeners.change.push(listener);
2048
1639
  }
2049
- /**
2050
- * Adds an event listener.
2051
- * @param {string} type The type of event to listen to. Can be 'change' or 'selection'
2052
- * @param {*} listener Function of the type (event) => {} to be called when the event occurs.
2053
- */
2054
-
2055
- }, {
2056
- key: "addEventListener",
2057
- value: function addEventListener(type, listener) {
2058
- if (type.match(/^(?:change|input)$/i)) {
2059
- this.listeners.change.push(listener);
2060
- }
2061
-
2062
- if (type.match(/^(?:selection|selectionchange)$/i)) {
2063
- this.listeners.selection.push(listener);
2064
- }
1640
+ if (type.match(/^(?:selection|selectionchange)$/i)) {
1641
+ this.listeners.selection.push(listener);
2065
1642
  }
2066
- }]);
2067
-
2068
- return Editor;
2069
- }();
2070
-
2071
- var _default = Editor;
2072
- exports.default = _default;
1643
+ }
1644
+ }
1645
+ var _default = exports.default = Editor;