tiny-markdown-editor 0.1.33 → 0.2.0

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