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