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