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