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