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