UNPKG

14.7 kBJavaScriptView Raw
1"use strict";
2
3var _toConsumableArray2 = require("babel-runtime/helpers/toConsumableArray");
4
5var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2);
6
7function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
8
9var PLACEHOLDERS = {
10 id: "__id__",
11 display: "__display__",
12 type: "__type__"
13};
14
15var escapeMap = {
16 '&': '&',
17 '<': '&lt;',
18 '>': '&gt;',
19 '"': '&quot;',
20 "'": '&#x27;',
21 '`': '&#x60;'
22};
23var createEscaper = function createEscaper(map) {
24 var escaper = function escaper(match) {
25 return map[match];
26 };
27 var keys = [];
28 for (var key in map) {
29 if (map.hasOwnProperty(key)) keys.push(key);
30 }
31 var source = '(?:' + keys.join('|') + ')';
32 var testRegexp = RegExp(source);
33 var replaceRegexp = RegExp(source, 'g');
34 return function (string) {
35 string = string == null ? '' : '' + string;
36 return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
37 };
38};
39
40var numericComparator = function numericComparator(a, b) {
41 a = a === null ? Number.MAX_VALUE : a;
42 b = b === null ? Number.MAX_VALUE : b;
43 return a - b;
44};
45
46module.exports = {
47
48 escapeHtml: createEscaper(escapeMap),
49
50 escapeRegex: function escapeRegex(str) {
51 return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
52 },
53
54 markupToRegex: function markupToRegex(markup, matchAtEnd) {
55 var markupPattern = this.escapeRegex(markup);
56 markupPattern = markupPattern.replace(PLACEHOLDERS.display, "(.+?)");
57 markupPattern = markupPattern.replace(PLACEHOLDERS.id, "(.+?)");
58 markupPattern = markupPattern.replace(PLACEHOLDERS.type, "(.+?)");
59 if (matchAtEnd) {
60 // append a $ to match at the end of the string
61 markupPattern = markupPattern + "$";
62 }
63 return new RegExp(markupPattern, "g");
64 },
65
66 spliceString: function spliceString(str, start, end, insert) {
67 return str.substring(0, start) + insert + str.substring(end);
68 },
69
70 extend: function extend(obj) {
71 var source, prop;
72 for (var i = 1, length = arguments.length; i < length; i++) {
73 source = arguments[i];
74 for (prop in source) {
75 if (hasOwnProperty.call(source, prop)) {
76 obj[prop] = source[prop];
77 }
78 }
79 }
80 return obj;
81 },
82
83 isNumber: function isNumber(obj) {
84 return Object.prototype.toString.call(obj) === "[object Number]";
85 },
86
87 /**
88 * parameterName: "id", "display", or "type"
89 */
90 getPositionOfCapturingGroup: function getPositionOfCapturingGroup(markup, parameterName) {
91 if (parameterName !== "id" && parameterName !== "display" && parameterName !== "type") {
92 throw new Error("parameterName must be 'id', 'display', or 'type'");
93 }
94
95 // calculate positions of placeholders in the markup
96 var indexDisplay = markup.indexOf(PLACEHOLDERS.display);
97 var indexId = markup.indexOf(PLACEHOLDERS.id);
98 var indexType = markup.indexOf(PLACEHOLDERS.type);
99
100 // set indices to null if not found
101 if (indexDisplay < 0) indexDisplay = null;
102 if (indexId < 0) indexId = null;
103 if (indexType < 0) indexType = null;
104
105 if (indexDisplay === null && indexId === null) {
106 // markup contains none of the mandatory placeholders
107 throw new Error("The markup `" + markup + "` must contain at least one of the placeholders `__id__` or `__display__`");
108 }
109
110 if (indexType === null && parameterName === "type") {
111 // markup does not contain optional __type__ placeholder
112 return null;
113 }
114
115 // sort indices in ascending order (null values will always be at the end)
116 var sortedIndices = [indexDisplay, indexId, indexType].sort(numericComparator);
117
118 // If only one the placeholders __id__ and __display__ is present,
119 // use the captured string for both parameters, id and display
120 if (indexDisplay === null) indexDisplay = indexId;
121 if (indexId === null) indexId = indexDisplay;
122
123 if (parameterName === "id") return sortedIndices.indexOf(indexId);
124 if (parameterName === "display") return sortedIndices.indexOf(indexDisplay);
125 if (parameterName === "type") return indexType === null ? null : sortedIndices.indexOf(indexType);
126 },
127
128 // Finds all occurences of the markup in the value and iterates the plain text sub strings
129 // in between those markups using `textIteratee` and the markup occurrences using the
130 // `markupIteratee`.
131 iterateMentionsMarkup: function iterateMentionsMarkup(value, markup, textIteratee, markupIteratee, displayTransform) {
132 var regex = this.markupToRegex(markup);
133 var displayPos = this.getPositionOfCapturingGroup(markup, "display");
134 var idPos = this.getPositionOfCapturingGroup(markup, "id");
135 var typePos = this.getPositionOfCapturingGroup(markup, "type");
136
137 var match;
138 var start = 0;
139 var currentPlainTextIndex = 0;
140
141 // detect all mention markup occurences in the value and iterate the matches
142 while ((match = regex.exec(value)) !== null) {
143
144 var id = match[idPos + 1];
145 var display = match[displayPos + 1];
146 var type = typePos !== null ? match[typePos + 1] : null;
147
148 if (displayTransform) display = displayTransform(id, display, type);
149
150 var substr = value.substring(start, match.index);
151 textIteratee(substr, start, currentPlainTextIndex);
152 currentPlainTextIndex += substr.length;
153
154 markupIteratee(match[0], match.index, currentPlainTextIndex, id, display, type, start);
155 currentPlainTextIndex += display.length;
156
157 start = regex.lastIndex;
158 }
159
160 if (start < value.length) {
161 textIteratee(value.substring(start), start, currentPlainTextIndex);
162 }
163 },
164
165 // For the passed character index in the plain text string, returns the corresponding index
166 // in the marked up value string.
167 // If the passed character index lies inside a mention, the value of `inMarkupCorrection` defines the
168 // correction to apply:
169 // - 'START' to return the index of the mention markup's first char (default)
170 // - 'END' to return the index after its last char
171 // - 'NULL' to return null
172 mapPlainTextIndex: function mapPlainTextIndex(value, markup, indexInPlainText) {
173 var inMarkupCorrection = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'START';
174 var displayTransform = arguments[4];
175
176 if (!this.isNumber(indexInPlainText)) {
177 return indexInPlainText;
178 }
179
180 var result;
181 var textIteratee = function textIteratee(substr, index, substrPlainTextIndex) {
182 if (result !== undefined) return;
183
184 if (substrPlainTextIndex + substr.length >= indexInPlainText) {
185 // found the corresponding position in the current plain text range
186 result = index + indexInPlainText - substrPlainTextIndex;
187 }
188 };
189 var markupIteratee = function markupIteratee(markup, index, mentionPlainTextIndex, id, display, type, lastMentionEndIndex) {
190 if (result !== undefined) return;
191
192 if (mentionPlainTextIndex + display.length > indexInPlainText) {
193 // found the corresponding position inside current match,
194 // return the index of the first or after the last char of the matching markup
195 // depending on whether the `inMarkupCorrection`
196 if (inMarkupCorrection === 'NULL') {
197 result = null;
198 } else {
199 result = index + (inMarkupCorrection === 'END' ? markup.length : 0);
200 }
201 }
202 };
203
204 this.iterateMentionsMarkup(value, markup, textIteratee, markupIteratee, displayTransform);
205
206 // when a mention is at the end of the value and we want to get the caret position
207 // at the end of the string, result is undefined
208 return result === undefined ? value.length : result;
209 },
210
211 // For a given indexInPlainText that lies inside a mention,
212 // returns a the index of of the first char of the mention in the plain text.
213 // If indexInPlainText does not lie inside a mention, returns indexInPlainText.
214 findStartOfMentionInPlainText: function findStartOfMentionInPlainText(value, markup, indexInPlainText, displayTransform) {
215 var result = indexInPlainText;
216 var foundMention = false;
217
218 var markupIteratee = function markupIteratee(markup, index, mentionPlainTextIndex, id, display, type, lastMentionEndIndex) {
219 if (mentionPlainTextIndex <= indexInPlainText && mentionPlainTextIndex + display.length > indexInPlainText) {
220 result = mentionPlainTextIndex;
221 foundMention = true;
222 }
223 };
224 this.iterateMentionsMarkup(value, markup, function () {}, markupIteratee, displayTransform);
225
226 if (foundMention) {
227 return result;
228 }
229 },
230
231 // Returns whether the given plain text index lies inside a mention
232 isInsideOfMention: function isInsideOfMention(value, markup, indexInPlainText, displayTransform) {
233 var mentionStart = this.findStartOfMentionInPlainText(value, markup, indexInPlainText, displayTransform);
234 return mentionStart !== undefined && mentionStart !== indexInPlainText;
235 },
236
237 // Applies a change from the plain text textarea to the underlying marked up value
238 // guided by the textarea text selection ranges before and after the change
239 applyChangeToValue: function applyChangeToValue(value, markup, plainTextValue, selectionStartBeforeChange, selectionEndBeforeChange, selectionEndAfterChange, displayTransform) {
240 var oldPlainTextValue = this.getPlainText(value, markup, displayTransform);
241
242 var lengthDelta = oldPlainTextValue.length - plainTextValue.length;
243 if (selectionStartBeforeChange === 'undefined') {
244 selectionStartBeforeChange = selectionEndAfterChange + lengthDelta;
245 }
246
247 if (selectionEndBeforeChange === 'undefined') {
248 selectionEndBeforeChange = selectionStartBeforeChange;
249 }
250
251 // Fixes an issue with replacing combined characters for complex input. Eg like acented letters on OSX
252 if (selectionStartBeforeChange === selectionEndBeforeChange && selectionEndBeforeChange === selectionEndAfterChange && oldPlainTextValue.length === plainTextValue.length) {
253 selectionStartBeforeChange = selectionStartBeforeChange - 1;
254 }
255
256 // extract the insertion from the new plain text value
257 var insert = plainTextValue.slice(selectionStartBeforeChange, selectionEndAfterChange);
258
259 // handling for Backspace key with no range selection
260 var spliceStart = Math.min(selectionStartBeforeChange, selectionEndAfterChange);
261
262 var spliceEnd = selectionEndBeforeChange;
263 if (selectionStartBeforeChange === selectionEndAfterChange) {
264 // handling for Delete key with no range selection
265 spliceEnd = Math.max(selectionEndBeforeChange, selectionStartBeforeChange + lengthDelta);
266 }
267
268 var mappedSpliceStart = this.mapPlainTextIndex(value, markup, spliceStart, 'START', displayTransform);
269 var mappedSpliceEnd = this.mapPlainTextIndex(value, markup, spliceEnd, 'END', displayTransform);
270
271 var controlSpliceStart = this.mapPlainTextIndex(value, markup, spliceStart, 'NULL', displayTransform);
272 var controlSpliceEnd = this.mapPlainTextIndex(value, markup, spliceEnd, 'NULL', displayTransform);
273 var willRemoveMention = controlSpliceStart === null || controlSpliceEnd === null;
274
275 var newValue = this.spliceString(value, mappedSpliceStart, mappedSpliceEnd, insert);
276
277 if (!willRemoveMention) {
278 // test for auto-completion changes
279 var controlPlainTextValue = this.getPlainText(newValue, markup, displayTransform);
280 if (controlPlainTextValue !== plainTextValue) {
281 // some auto-correction is going on
282
283 // find start of diff
284 spliceStart = 0;
285 while (plainTextValue[spliceStart] === controlPlainTextValue[spliceStart]) {
286 spliceStart++;
287 } // extract auto-corrected insertion
288 insert = plainTextValue.slice(spliceStart, selectionEndAfterChange);
289
290 // find index of the unchanged remainder
291 spliceEnd = oldPlainTextValue.lastIndexOf(plainTextValue.substring(selectionEndAfterChange));
292
293 // re-map the corrected indices
294 mappedSpliceStart = this.mapPlainTextIndex(value, markup, spliceStart, 'START', displayTransform);
295 mappedSpliceEnd = this.mapPlainTextIndex(value, markup, spliceEnd, 'END', displayTransform);
296 newValue = this.spliceString(value, mappedSpliceStart, mappedSpliceEnd, insert);
297 }
298 }
299
300 return newValue;
301 },
302
303 getPlainText: function getPlainText(value, markup, displayTransform) {
304 var regex = this.markupToRegex(markup);
305 var idPos = this.getPositionOfCapturingGroup(markup, "id");
306 var displayPos = this.getPositionOfCapturingGroup(markup, "display");
307 var typePos = this.getPositionOfCapturingGroup(markup, "type");
308 return value.replace(regex, function () {
309 // first argument is the whole match, capturing groups are following
310 var id = arguments[idPos + 1];
311 var display = arguments[displayPos + 1];
312 var type = arguments[typePos + 1];
313 if (displayTransform) display = displayTransform(id, display, type);
314 return display;
315 });
316 },
317
318 getMentions: function getMentions(value, markup) {
319 var mentions = [];
320 this.iterateMentionsMarkup(value, markup, function () {}, function (match, index, plainTextIndex, id, display, type, start) {
321 mentions.push({
322 id: id,
323 display: display,
324 type: type,
325 index: index,
326 plainTextIndex: plainTextIndex
327 });
328 });
329 return mentions;
330 },
331
332 makeMentionsMarkup: function makeMentionsMarkup(markup, id, display, type) {
333 var result = markup.replace(PLACEHOLDERS.id, id);
334 result = result.replace(PLACEHOLDERS.display, display);
335 result = result.replace(PLACEHOLDERS.type, type);
336 return result;
337 },
338
339 countSuggestions: function countSuggestions(suggestions) {
340 var result = 0;
341 for (var prop in suggestions) {
342 if (suggestions.hasOwnProperty(prop)) {
343 result += suggestions[prop].results.length;
344 }
345 }
346 return result;
347 },
348
349 getSuggestions: function getSuggestions(suggestions) {
350 var result = [];
351
352 for (var mentionType in suggestions) {
353 if (!suggestions.hasOwnProperty(mentionType)) {
354 return;
355 }
356
357 result = result.concat({
358 suggestions: suggestions[mentionType].results,
359 descriptor: suggestions[mentionType]
360 });
361 }
362
363 return result;
364 },
365
366 getSuggestion: function getSuggestion(suggestions, index) {
367 return this.getSuggestions(suggestions).reduce(function (result, _ref) {
368 var suggestions = _ref.suggestions,
369 descriptor = _ref.descriptor;
370 return [].concat((0, _toConsumableArray3.default)(result), (0, _toConsumableArray3.default)(suggestions.map(function (suggestion) {
371 return {
372 suggestion: suggestion,
373 descriptor: descriptor
374 };
375 })));
376 }, [])[index];
377 }
378
379};
\No newline at end of file