react-headless-mde 2.0.3 → 3.0.0-beta1

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -18
  3. package/dist/cjs/index.js +338 -262
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/esm/commands/base-command.d.ts +24 -0
  6. package/dist/esm/commands/line/heading-level1.d.ts +4 -0
  7. package/dist/esm/commands/line/heading-level2.d.ts +4 -0
  8. package/dist/esm/commands/line/heading-level3.d.ts +4 -0
  9. package/dist/esm/commands/line/heading-level4.d.ts +4 -0
  10. package/dist/esm/commands/line/heading-level5.d.ts +4 -0
  11. package/dist/esm/commands/line/heading-level6.d.ts +4 -0
  12. package/dist/esm/commands/line/line-prefix-command.d.ts +7 -0
  13. package/dist/esm/commands/line/quote.d.ts +4 -0
  14. package/dist/esm/commands/list/checked-list.d.ts +4 -0
  15. package/dist/esm/commands/list/list-command.d.ts +5 -0
  16. package/dist/esm/commands/list/ordered-list.d.ts +4 -0
  17. package/dist/esm/commands/list/unordered-list.d.ts +4 -0
  18. package/dist/esm/commands/word/bold.d.ts +5 -0
  19. package/dist/esm/commands/word/code-block.d.ts +4 -0
  20. package/dist/esm/commands/word/code.d.ts +5 -0
  21. package/dist/esm/commands/word/italic.d.ts +5 -0
  22. package/dist/esm/commands/word/strikethrough.d.ts +5 -0
  23. package/dist/esm/commands/word/wrap-command.d.ts +11 -0
  24. package/dist/esm/commands/word-complex/image.d.ts +4 -0
  25. package/dist/esm/commands/word-complex/link.d.ts +4 -0
  26. package/dist/esm/controllers/textarea-controller.d.ts +15 -0
  27. package/dist/esm/hooks/use-markdown-editor.d.ts +12 -0
  28. package/dist/esm/index.d.ts +27 -0
  29. package/dist/esm/index.js +319 -243
  30. package/dist/esm/index.js.map +1 -1
  31. package/dist/esm/types/command.d.ts +10 -0
  32. package/dist/esm/types/text-controller.d.ts +18 -0
  33. package/dist/esm/utils/insert-text-to-selection.d.ts +8 -0
  34. package/dist/esm/utils/selection-and-text.d.ts +14 -0
  35. package/dist/index.d.ts +121 -66
  36. package/package.json +58 -46
  37. package/.eslintrc.js +0 -24
  38. package/.github/workflows/npm-publish.yml +0 -27
  39. package/.husky/pre-commit +0 -4
  40. package/.husky/pre-push +0 -4
  41. package/jest.config.js +0 -4
  42. package/lint-staged.config.js +0 -4
  43. package/prettier.config.js +0 -13
  44. package/rollup.config.js +0 -31
  45. package/vite.config.js +0 -7
package/dist/cjs/index.js CHANGED
@@ -1,29 +1,7 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
3
  var react = require('react');
6
4
 
7
- class CommandController {
8
- constructor(textController, commandMap) {
9
- this.textController = textController;
10
- this.commandMap = commandMap;
11
- }
12
- executeCommand(commandName) {
13
- var _a;
14
- const command = this.commandMap[commandName];
15
- if (!command) {
16
- throw new Error(`Cannot execute command. Command not found: ${commandName}`);
17
- }
18
- if (command.undo && ((_a = command.shouldUndo) === null || _a === void 0 ? void 0 : _a.call(command, this.textController))) {
19
- command.undo(this.textController);
20
- }
21
- else {
22
- command.do(this.textController);
23
- }
24
- }
25
- }
26
-
27
5
  /**
28
6
  * Inserts the given text at the cursor. If the element contains a selection, the selection
29
7
  * will be replaced by the text.
@@ -122,8 +100,6 @@ function canManipulateViaTextNodes(input) {
122
100
  return browserSupportsTextareaTextNodes;
123
101
  }
124
102
 
125
- // A list of helpers for manipulating Markdown text.
126
- // These helpers do not interface with a textarea.
127
103
  // Check if char is "space" or "new line".
128
104
  // Char is optional because we pass a char by index,
129
105
  // and theoretically index can be outside the string range. See text[i - 1] and text[i].
@@ -172,6 +148,16 @@ function selectAfterWord({ text, selection }) {
172
148
  }
173
149
  return selection;
174
150
  }
151
+ // Expands the selection to cover the whole lines it touches:
152
+ // the start moves to the beginning of the first line, the end — to the end of the last one
153
+ function getLineSelection({ text, selection }) {
154
+ let { start, end } = selection;
155
+ while (start > 0 && text[start - 1] !== '\n')
156
+ start--;
157
+ while (end < text.length && text[end] !== '\n')
158
+ end++;
159
+ return { start, end };
160
+ }
175
161
  // Gets the number of line-breaks that would have to be inserted before the given 'startPosition'
176
162
  // to make sure there's an empty line between 'startPosition' and the previous text
177
163
  function getBreaksNeededForEmptyLineBefore(text = '', startPosition) {
@@ -195,7 +181,7 @@ function getBreaksNeededForEmptyLineBefore(text = '', startPosition) {
195
181
  return neededBreaks;
196
182
  }
197
183
  }
198
- return isInFirstLine ? 0 : neededBreaks;
184
+ return isInFirstLine ? 0 : Math.max(0, neededBreaks);
199
185
  }
200
186
  // Gets the number of line-breaks that would have to be inserted after the given 'startPosition'
201
187
  // to make sure there's an empty line between 'startPosition' and the next text
@@ -221,7 +207,7 @@ function getBreaksNeededForEmptyLineAfter(text = '', startPosition) {
221
207
  return neededBreaks;
222
208
  }
223
209
  }
224
- return isInLastLine ? 0 : neededBreaks;
210
+ return isInLastLine ? 0 : Math.max(0, neededBreaks);
225
211
  }
226
212
  function getSelectedText(textSection) {
227
213
  return textSection.text.slice(textSection.selection.start, textSection.selection.end);
@@ -255,14 +241,15 @@ function insertBeforeEachLine(selectedText, insertBefore) {
255
241
 
256
242
  var selectionAndText = /*#__PURE__*/Object.freeze({
257
243
  __proto__: null,
258
- getWordSelection: getWordSelection,
259
- selectAfterWord: selectAfterWord,
260
- getBreaksNeededForEmptyLineBefore: getBreaksNeededForEmptyLineBefore,
261
244
  getBreaksNeededForEmptyLineAfter: getBreaksNeededForEmptyLineAfter,
245
+ getBreaksNeededForEmptyLineBefore: getBreaksNeededForEmptyLineBefore,
246
+ getLineSelection: getLineSelection,
262
247
  getSelectedText: getSelectedText,
263
- getStringBeforeSelection: getStringBeforeSelection,
264
248
  getStringAfterSelection: getStringAfterSelection,
265
- insertBeforeEachLine: insertBeforeEachLine
249
+ getStringBeforeSelection: getStringBeforeSelection,
250
+ getWordSelection: getWordSelection,
251
+ insertBeforeEachLine: insertBeforeEachLine,
252
+ selectAfterWord: selectAfterWord
266
253
  });
267
254
 
268
255
  function getStateFromTextArea(textArea) {
@@ -320,9 +307,9 @@ class TextareaController {
320
307
  const state1 = this.getState();
321
308
  // Replaces the current selection with new string
322
309
  const state2 = this.replaceSelection(`${prefix}${getSelectedText(state1)}${suffix}`);
323
- // Adjust the selection to not contain the **
310
+ // Adjust the selection to not contain the prefix and suffix
324
311
  return this.setSelection({
325
- start: state2.selection.end - prefix.length - getSelectedText(state1).length,
312
+ start: state2.selection.end - suffix.length - getSelectedText(state1).length,
326
313
  end: state2.selection.end - suffix.length,
327
314
  });
328
315
  }
@@ -353,283 +340,372 @@ class TextareaController {
353
340
 
354
341
  function useTextAreaMarkdownEditor(options) {
355
342
  const textAreaRef = react.useRef(null);
343
+ // Always execute commands from the most recent render
344
+ const commandMapRef = react.useRef(options.commandMap);
345
+ react.useEffect(() => {
346
+ commandMapRef.current = options.commandMap;
347
+ });
348
+ // Command instances persist between executor rebuilds
349
+ const instancesRef = react.useRef(new Map());
350
+ // Rebuild the executors object only when the set of command names changes,
351
+ // so `commands` (and each executor) stays referentially stable between renders
352
+ const commandNames = Object.keys(options.commandMap).sort().join('\0');
356
353
  return react.useMemo(() => {
357
354
  const textController = new TextareaController(textAreaRef);
358
- const commandController = new CommandController(textController, options.commandMap);
355
+ const instances = instancesRef.current;
356
+ const executors = {};
357
+ for (const name of commandNames.split('\0')) {
358
+ if (name === '')
359
+ continue;
360
+ executors[name] = (context) => {
361
+ var _a;
362
+ const CommandClass = commandMapRef.current[name];
363
+ if (CommandClass === undefined) {
364
+ throw new Error(`Cannot execute command. Command not found: ${name}`);
365
+ }
366
+ let instance = instances.get(name);
367
+ if (instance === undefined || instance.constructor !== CommandClass) {
368
+ instance = new CommandClass(textController);
369
+ instances.set(name, instance);
370
+ }
371
+ if (instance.undo !== undefined && ((_a = instance.shouldUndo) === null || _a === void 0 ? void 0 : _a.call(instance, context)) === true) {
372
+ instance.undo(context);
373
+ }
374
+ else {
375
+ instance.do(context);
376
+ }
377
+ };
378
+ }
359
379
  return {
360
380
  ref: textAreaRef,
361
381
  textController,
362
- commandController,
382
+ commands: executors,
363
383
  };
364
- }, [textAreaRef]);
384
+ }, [commandNames]);
365
385
  }
366
386
 
367
- function makeList(state0, textController, insertBefore) {
368
- // Adjust the selection to encompass the whole word if the caret is inside one
369
- const newSelectionRange = getWordSelection({
370
- text: state0.text,
371
- selection: state0.selection,
372
- });
373
- const state1 = textController.setSelection(newSelectionRange);
374
- const breaksBeforeCount = getBreaksNeededForEmptyLineBefore(state1.text, state1.selection.start);
375
- const breaksBefore = Array(breaksBeforeCount + 1).join('\n');
376
- const breaksAfterCount = getBreaksNeededForEmptyLineAfter(state1.text, state1.selection.end);
377
- const breaksAfter = Array(breaksAfterCount + 1).join('\n');
378
- const modifiedText = insertBeforeEachLine(getSelectedText(state1), insertBefore);
379
- textController.replaceSelection(`${breaksBefore}${modifiedText.modifiedText}${breaksAfter}`);
380
- // Specifically when the text has only one line, we can exclude the "- ", for example, from the selection
381
- const oneLinerOffset = !getSelectedText(state1).includes('\n') ? modifiedText.insertionLength : 0;
382
- const selectionStart = state1.selection.start + breaksBeforeCount + oneLinerOffset;
383
- const selectionEnd = selectionStart + modifiedText.modifiedText.length - oneLinerOffset;
384
- // Adjust the selection to not contain the **
385
- textController.setSelection({
386
- start: selectionStart,
387
- end: selectionEnd,
388
- });
387
+ /**
388
+ * Base class for all commands.
389
+ *
390
+ * `Context` is the type of the value that must be passed when the command is executed.
391
+ * Commands without a context extend `BaseCommand` (Context = void) and are executed
392
+ * without arguments; commands with a context extend `BaseCommand<Context>` and require
393
+ * an argument of that type:
394
+ *
395
+ * class AttachmentCommand extends BaseCommand<Promise<string>> {
396
+ * async do(upload: Promise<string>) {
397
+ * const url = await upload;
398
+ * this.textController.replaceSelection(`![](${url})`);
399
+ * }
400
+ * }
401
+ */
402
+ class BaseCommand {
403
+ constructor(textController) {
404
+ this.textController = textController;
405
+ }
389
406
  }
390
407
 
391
- var list = /*#__PURE__*/Object.freeze({
392
- __proto__: null,
393
- makeList: makeList
394
- });
395
-
396
- function setHeader(initialState, api, prefix) {
397
- // Adjust the selection to encompass the whole word if the caret is inside one
398
- const newSelectionRange = getWordSelection({
399
- text: initialState.text,
400
- selection: initialState.selection,
401
- });
402
- const state1 = api.setSelection(newSelectionRange);
403
- // Add the prefix to the selection
404
- const state2 = api.replaceSelection(`${prefix}${getSelectedText(state1)}`);
405
- // Adjust the selection to not contain the prefix
406
- api.setSelection({
407
- start: state2.selection.end - getSelectedText(state1).length,
408
- end: state2.selection.end,
409
- });
408
+ // Base class for commands that wrap a word/selection with prefix and suffix: bold, italic etc.
409
+ class WrapCommand extends BaseCommand {
410
+ do() {
411
+ // Adjust the selection to encompass the whole word if the caret is inside one
412
+ this.textController.selectWordByCursor();
413
+ this.textController.wrapSelection(this.prefix, this.suffix);
414
+ }
415
+ shouldUndo() {
416
+ const state = this.getWordSelectionState();
417
+ return this.hasMarkersInside(getSelectedText(state)) || this.hasMarkersOutside(state);
418
+ }
419
+ undo() {
420
+ const state = this.getWordSelectionState();
421
+ const selectedText = getSelectedText(state);
422
+ this.textController.setSelection(state.selection);
423
+ if (!this.hasMarkersInside(selectedText)) {
424
+ // The markers surround the selection: `**[word]**`
425
+ this.textController.unwrapSelection(this.prefix.length, this.suffix.length);
426
+ return;
427
+ }
428
+ // The markers are inside the selection — the caret is within `**wo|rd**`:
429
+ // strip the markers from the edges of the selection
430
+ const innerText = selectedText.slice(this.prefix.length, selectedText.length - this.suffix.length);
431
+ const replacedState = this.textController.replaceSelection(innerText);
432
+ this.textController.setSelection({
433
+ start: replacedState.selection.end - innerText.length,
434
+ end: replacedState.selection.end,
435
+ });
436
+ }
437
+ getWordSelectionState() {
438
+ const state = this.textController.getState();
439
+ return {
440
+ text: state.text,
441
+ selection: getWordSelection(state),
442
+ };
443
+ }
444
+ hasMarkersInside(selectedText) {
445
+ return (selectedText.length >= this.prefix.length + this.suffix.length &&
446
+ selectedText.startsWith(this.prefix) &&
447
+ selectedText.endsWith(this.suffix));
448
+ }
449
+ hasMarkersOutside({ text, selection }) {
450
+ const markersMatch = getStringBeforeSelection({ text, selection }, this.prefix.length) === this.prefix &&
451
+ getStringAfterSelection({ text, selection }, this.suffix.length) === this.suffix;
452
+ if (!markersMatch)
453
+ return false;
454
+ // The characters right next to the markers must not continue the marker sequence:
455
+ // `*` around `[word]` inside `**word**` is bold, not italic
456
+ const charBeforeMarkers = text[selection.start - this.prefix.length - 1];
457
+ const charAfterMarkers = text[selection.end + this.suffix.length];
458
+ return charBeforeMarkers !== this.prefix[0] && charAfterMarkers !== this.suffix[this.suffix.length - 1];
459
+ }
410
460
  }
411
461
 
412
- var header = /*#__PURE__*/Object.freeze({
413
- __proto__: null,
414
- setHeader: setHeader
415
- });
416
-
417
- const boldCommand = {
418
- shouldUndo: textCtrl => {
419
- const wordSelectionState = textCtrl.selectWordByCursor();
420
- return (getStringBeforeSelection(wordSelectionState, 2) === '**' &&
421
- getStringAfterSelection(wordSelectionState, 2) === '**');
422
- },
423
- do: textCtrl => {
424
- textCtrl.wrapSelection('**', '**');
425
- },
426
- undo: textCtrl => {
427
- textCtrl.unwrapSelection(2, 2);
428
- },
429
- };
462
+ // Base class for line-level commands that prefix each selected line: quote, headings.
463
+ // The selection is expanded to cover the whole lines it touches.
464
+ // Executing the command on already prefixed lines removes the prefixes (undo).
465
+ class LinePrefixCommand extends BaseCommand {
466
+ do() {
467
+ const state = this.textController.getState();
468
+ const state1 = this.textController.setSelection(getLineSelection(state));
469
+ const selectedText = getSelectedText(state1);
470
+ const { modifiedText } = insertBeforeEachLine(selectedText, this.prefix);
471
+ const state2 = this.textController.replaceSelection(modifiedText);
472
+ // For a single line select the content without the prefix,
473
+ // for multiple lines select the whole modified block
474
+ const prefixOffset = selectedText.includes('\n') ? 0 : this.prefix.length;
475
+ this.textController.setSelection({
476
+ start: state2.selection.end - modifiedText.length + prefixOffset,
477
+ end: state2.selection.end,
478
+ });
479
+ }
480
+ shouldUndo() {
481
+ const state = this.textController.getState();
482
+ const selectedText = getSelectedText({
483
+ text: state.text,
484
+ selection: getLineSelection(state),
485
+ });
486
+ return selectedText.split('\n').every(line => line.startsWith(this.prefix));
487
+ }
488
+ undo() {
489
+ const state = this.textController.getState();
490
+ const state1 = this.textController.setSelection(getLineSelection(state));
491
+ const modifiedText = getSelectedText(state1)
492
+ .split('\n')
493
+ .map(line => (line.startsWith(this.prefix) ? line.slice(this.prefix.length) : line))
494
+ .join('\n');
495
+ this.textController.replaceSelection(modifiedText);
496
+ // Select the content of the un-prefixed line(s)
497
+ this.textController.setSelection({
498
+ start: state1.selection.start,
499
+ end: state1.selection.start + modifiedText.length,
500
+ });
501
+ }
502
+ }
430
503
 
431
- const italicCommand = {
432
- shouldUndo: textCtrl => {
433
- const wordSelectionState = textCtrl.selectWordByCursor();
434
- return (getStringBeforeSelection(wordSelectionState, 1) === '*' && getStringAfterSelection(wordSelectionState, 1) === '*');
435
- },
436
- do(textCtrl) {
437
- textCtrl.wrapSelection('*', '*');
438
- },
439
- undo(textCtrl) {
440
- textCtrl.unwrapSelection(1, 1);
441
- },
442
- };
504
+ // Base class for list commands. Each selected line becomes a list item.
505
+ // The selection is expanded to cover the whole lines it touches; when a list is
506
+ // inserted in the middle of a paragraph, it is separated by empty lines.
507
+ class ListCommand extends BaseCommand {
508
+ do() {
509
+ const state0 = this.textController.getState();
510
+ const state1 = this.textController.setSelection(getLineSelection(state0));
511
+ const breaksBeforeCount = getBreaksNeededForEmptyLineBefore(state1.text, state1.selection.start);
512
+ const breaksBefore = '\n'.repeat(breaksBeforeCount);
513
+ const breaksAfterCount = getBreaksNeededForEmptyLineAfter(state1.text, state1.selection.end);
514
+ const breaksAfter = '\n'.repeat(breaksAfterCount);
515
+ const selectedText = getSelectedText(state1);
516
+ const { modifiedText } = insertBeforeEachLine(selectedText, (line, index) => this.getLinePrefix(line, index));
517
+ this.textController.replaceSelection(`${breaksBefore}${modifiedText}${breaksAfter}`);
518
+ // For a single line select the content without the prefix,
519
+ // for multiple lines select the whole modified block
520
+ const prefixOffset = selectedText.includes('\n') ? 0 : this.getLinePrefix('', 0).length;
521
+ const blockStart = state1.selection.start + breaksBeforeCount;
522
+ this.textController.setSelection({
523
+ start: blockStart + prefixOffset,
524
+ end: blockStart + modifiedText.length,
525
+ });
526
+ }
527
+ }
443
528
 
444
- const strikethroughCommand = {
445
- shouldUndo(textCtrl) {
446
- const wordSelectionState = textCtrl.selectWordByCursor();
447
- return (getStringBeforeSelection(wordSelectionState, 2) === '~~' &&
448
- getStringAfterSelection(wordSelectionState, 2) === '~~');
449
- },
450
- do(textCtrl) {
451
- textCtrl.wrapSelection('~~', '~~');
452
- },
453
- undo(textCtrl) {
454
- textCtrl.unwrapSelection(2, 2);
455
- },
456
- };
529
+ class BoldCommand extends WrapCommand {
530
+ constructor() {
531
+ super(...arguments);
532
+ this.prefix = '**';
533
+ this.suffix = '**';
534
+ }
535
+ }
457
536
 
458
- const linkCommand = {
459
- do: textApi => {
460
- // Adjust the selection to encompass the whole word if the caret is inside one
461
- const wordSelectionState = textApi.selectWordByCursor();
462
- // Replaces the current selection with the bold mark up
463
- const state2 = textApi.replaceSelection(`[${getSelectedText(wordSelectionState)}](url)`);
464
- // Adjust the selection to not contain the **
465
- textApi.setSelection({
466
- start: state2.selection.end - 6 - getSelectedText(wordSelectionState).length,
467
- end: state2.selection.end - 6,
468
- });
469
- },
470
- };
537
+ class ItalicCommand extends WrapCommand {
538
+ constructor() {
539
+ super(...arguments);
540
+ this.prefix = '*';
541
+ this.suffix = '*';
542
+ }
543
+ }
471
544
 
472
- // Todo: try to rewrite with setHeader()
473
- const quoteCommand = {
474
- do: textApi => {
475
- // Adjust the selection to encompass the whole word if the caret is inside one
476
- const state = textApi.selectWordByCursor();
477
- const breaksBeforeCount = getBreaksNeededForEmptyLineBefore(state.text, state.selection.start);
478
- const breaksBefore = Array(breaksBeforeCount + 1).join('\n');
479
- const breaksAfterCount = getBreaksNeededForEmptyLineAfter(state.text, state.selection.end);
480
- const breaksAfter = Array(breaksAfterCount + 1).join('\n');
481
- // Replaces the current selection with the quote mark up
482
- textApi.replaceSelection(`${breaksBefore}> ${getSelectedText(state)}${breaksAfter}`);
483
- const selectionStart = state.selection.start + breaksBeforeCount + 2;
484
- const selectionEnd = selectionStart + getSelectedText(state).length;
485
- textApi.setSelection({
486
- start: selectionStart,
487
- end: selectionEnd,
488
- });
489
- },
490
- };
545
+ class StrikethroughCommand extends WrapCommand {
546
+ constructor() {
547
+ super(...arguments);
548
+ this.prefix = '~~';
549
+ this.suffix = '~~';
550
+ }
551
+ }
491
552
 
492
- const codeCommand = {
493
- shouldUndo: textCtrl => {
494
- const wordSelectionState = textCtrl.selectWordByCursor();
495
- return (getStringBeforeSelection(wordSelectionState, 1) === '`' && getStringAfterSelection(wordSelectionState, 1) === '`');
496
- },
497
- do(textCtrl) {
498
- textCtrl.wrapSelection('`', '`');
499
- },
500
- undo(textCtrl) {
501
- textCtrl.unwrapSelection(1, 1);
502
- },
503
- };
553
+ class CodeCommand extends WrapCommand {
554
+ constructor() {
555
+ super(...arguments);
556
+ this.prefix = '`';
557
+ this.suffix = '`';
558
+ }
559
+ }
504
560
 
505
- const codeBlockCommand = {
506
- do: textApi => {
561
+ class CodeBlockCommand extends BaseCommand {
562
+ do() {
563
+ const textApi = this.textController;
507
564
  // Adjust the selection to encompass the whole word if the caret is inside one
508
565
  const state1 = textApi.selectWordByCursor();
509
- // when there's no breaking line
510
- if (!getSelectedText(state1).includes('\n')) {
511
- textApi.replaceSelection(`\`${getSelectedText(state1)}\``);
512
- // Adjust the selection to not contain the **
513
- const selectionStart = state1.selection.start + 1;
514
- const selectionEnd = selectionStart + getSelectedText(state1).length;
566
+ const selectedText = getSelectedText(state1);
567
+ // when there's no breaking line — inline code
568
+ if (!selectedText.includes('\n')) {
569
+ textApi.replaceSelection(`\`${selectedText}\``);
515
570
  textApi.setSelection({
516
- start: selectionStart,
517
- end: selectionEnd,
571
+ start: state1.selection.start + 1,
572
+ end: state1.selection.start + 1 + selectedText.length,
518
573
  });
519
574
  return;
520
575
  }
521
576
  const breaksBeforeCount = getBreaksNeededForEmptyLineBefore(state1.text, state1.selection.start);
522
- const breaksBefore = Array(breaksBeforeCount + 1).join('\n');
577
+ const breaksBefore = '\n'.repeat(breaksBeforeCount);
523
578
  const breaksAfterCount = getBreaksNeededForEmptyLineAfter(state1.text, state1.selection.end);
524
- const breaksAfter = Array(breaksAfterCount + 1).join('\n');
525
- textApi.replaceSelection(`${breaksBefore}\`\`\`\n${getSelectedText(state1)}\n\`\`\`${breaksAfter}`);
579
+ const breaksAfter = '\n'.repeat(breaksAfterCount);
580
+ textApi.replaceSelection(`${breaksBefore}\`\`\`\n${selectedText}\n\`\`\`${breaksAfter}`);
526
581
  const selectionStart = state1.selection.start + breaksBeforeCount + 4;
527
- const selectionEnd = selectionStart + getSelectedText(state1).length;
528
582
  textApi.setSelection({
529
583
  start: selectionStart,
530
- end: selectionEnd,
584
+ end: selectionStart + selectedText.length,
531
585
  });
532
- },
533
- };
534
-
535
- const checkedListCommand = {
536
- do: textApi => {
537
- const initialState = textApi.getState();
538
- makeList(initialState, textApi, () => `- [ ] `);
539
- },
540
- };
541
-
542
- const orderedListCommand = {
543
- do: textApi => {
544
- const initialState = textApi.getState();
545
- makeList(initialState, textApi, (item, index) => `${index + 1}. `);
546
- },
547
- };
586
+ }
587
+ }
548
588
 
549
- const unorderedListCommand = {
550
- do: textApi => {
551
- const initialState = textApi.getState();
552
- makeList(initialState, textApi, '- ');
553
- },
554
- };
589
+ class LinkCommand extends BaseCommand {
590
+ do() {
591
+ const textApi = this.textController;
592
+ // Adjust the selection to encompass the whole word if the caret is inside one
593
+ const wordSelectionState = textApi.selectWordByCursor();
594
+ // Replaces the current selection with the link markup
595
+ const state2 = textApi.replaceSelection(`[${getSelectedText(wordSelectionState)}](url)`);
596
+ // Select the `url` placeholder so it can be replaced right away
597
+ textApi.setSelection({
598
+ start: state2.selection.end - 4,
599
+ end: state2.selection.end - 1,
600
+ });
601
+ }
602
+ }
555
603
 
556
- const imageCommand = {
557
- do: textApi => {
558
- // Replaces the current selection with the whole word selected
604
+ class ImageCommand extends BaseCommand {
605
+ do() {
606
+ const textApi = this.textController;
607
+ // Adjust the selection to encompass the whole word if the caret is inside one
559
608
  const wordSelectionState = textApi.selectWordByCursor();
560
609
  // Replaces the current selection with the image
561
610
  const imageTemplate = getSelectedText(wordSelectionState) || 'https://example.com/your-image.png';
562
611
  textApi.replaceSelection(`![](${imageTemplate})`);
563
- // Adjust the selection to not contain the **
612
+ // Select the image url so it can be replaced right away
564
613
  textApi.setSelection({
565
614
  start: wordSelectionState.selection.start + 4,
566
615
  end: wordSelectionState.selection.start + 4 + imageTemplate.length,
567
616
  });
568
- },
569
- };
617
+ }
618
+ }
570
619
 
571
- const headingLevel1Command = {
572
- do: textCtrl => {
573
- const initialState = textCtrl.getState();
574
- setHeader(initialState, textCtrl, '# ');
575
- },
576
- };
620
+ class QuoteCommand extends LinePrefixCommand {
621
+ constructor() {
622
+ super(...arguments);
623
+ this.prefix = '> ';
624
+ }
625
+ }
577
626
 
578
- const headingLevel2Command = {
579
- do: textCtrl => {
580
- const initialState = textCtrl.getState();
581
- setHeader(initialState, textCtrl, '## ');
582
- },
583
- };
627
+ class HeadingLevel1Command extends LinePrefixCommand {
628
+ constructor() {
629
+ super(...arguments);
630
+ this.prefix = '# ';
631
+ }
632
+ }
584
633
 
585
- const headingLevel3Command = {
586
- do: textCtrl => {
587
- const initialState = textCtrl.getState();
588
- setHeader(initialState, textCtrl, '### ');
589
- },
590
- };
634
+ class HeadingLevel2Command extends LinePrefixCommand {
635
+ constructor() {
636
+ super(...arguments);
637
+ this.prefix = '## ';
638
+ }
639
+ }
591
640
 
592
- const headingLevel4Command = {
593
- do: textCtrl => {
594
- const initialState = textCtrl.getState();
595
- setHeader(initialState, textCtrl, '#### ');
596
- },
597
- };
641
+ class HeadingLevel3Command extends LinePrefixCommand {
642
+ constructor() {
643
+ super(...arguments);
644
+ this.prefix = '### ';
645
+ }
646
+ }
598
647
 
599
- const headingLevel5Command = {
600
- do: textCtrl => {
601
- const initialState = textCtrl.getState();
602
- setHeader(initialState, textCtrl, '##### ');
603
- },
604
- };
648
+ class HeadingLevel4Command extends LinePrefixCommand {
649
+ constructor() {
650
+ super(...arguments);
651
+ this.prefix = '#### ';
652
+ }
653
+ }
605
654
 
606
- const headingLevel6Command = {
607
- do: textCtrl => {
608
- const initialState = textCtrl.getState();
609
- setHeader(initialState, textCtrl, '###### ');
610
- },
611
- };
655
+ class HeadingLevel5Command extends LinePrefixCommand {
656
+ constructor() {
657
+ super(...arguments);
658
+ this.prefix = '##### ';
659
+ }
660
+ }
661
+
662
+ class HeadingLevel6Command extends LinePrefixCommand {
663
+ constructor() {
664
+ super(...arguments);
665
+ this.prefix = '###### ';
666
+ }
667
+ }
668
+
669
+ class UnorderedListCommand extends ListCommand {
670
+ getLinePrefix() {
671
+ return '- ';
672
+ }
673
+ }
674
+
675
+ class OrderedListCommand extends ListCommand {
676
+ getLinePrefix(_line, index) {
677
+ return `${index + 1}. `;
678
+ }
679
+ }
680
+
681
+ class CheckedListCommand extends ListCommand {
682
+ getLinePrefix() {
683
+ return '- [ ] ';
684
+ }
685
+ }
612
686
 
687
+ exports.BaseCommand = BaseCommand;
688
+ exports.BoldCommand = BoldCommand;
689
+ exports.CheckedListCommand = CheckedListCommand;
690
+ exports.CodeBlockCommand = CodeBlockCommand;
691
+ exports.CodeCommand = CodeCommand;
692
+ exports.HeadingLevel1Command = HeadingLevel1Command;
693
+ exports.HeadingLevel2Command = HeadingLevel2Command;
694
+ exports.HeadingLevel3Command = HeadingLevel3Command;
695
+ exports.HeadingLevel4Command = HeadingLevel4Command;
696
+ exports.HeadingLevel5Command = HeadingLevel5Command;
697
+ exports.HeadingLevel6Command = HeadingLevel6Command;
698
+ exports.ImageCommand = ImageCommand;
699
+ exports.ItalicCommand = ItalicCommand;
700
+ exports.LinePrefixCommand = LinePrefixCommand;
701
+ exports.LinkCommand = LinkCommand;
702
+ exports.ListCommand = ListCommand;
703
+ exports.OrderedListCommand = OrderedListCommand;
704
+ exports.QuoteCommand = QuoteCommand;
705
+ exports.StrikethroughCommand = StrikethroughCommand;
613
706
  exports.TextareaController = TextareaController;
614
- exports.boldCommand = boldCommand;
615
- exports.checkedListCommand = checkedListCommand;
616
- exports.codeBlockCommand = codeBlockCommand;
617
- exports.codeCommand = codeCommand;
618
- exports.headerHelpers = header;
619
- exports.headingLevel1Command = headingLevel1Command;
620
- exports.headingLevel2Command = headingLevel2Command;
621
- exports.headingLevel3Command = headingLevel3Command;
622
- exports.headingLevel4Command = headingLevel4Command;
623
- exports.headingLevel5Command = headingLevel5Command;
624
- exports.headingLevel6Command = headingLevel6Command;
625
- exports.imageCommand = imageCommand;
626
- exports.italicCommand = italicCommand;
627
- exports.linkCommand = linkCommand;
628
- exports.listHelpers = list;
629
- exports.orderedListCommand = orderedListCommand;
630
- exports.quoteCommand = quoteCommand;
631
- exports.strikethroughCommand = strikethroughCommand;
707
+ exports.UnorderedListCommand = UnorderedListCommand;
708
+ exports.WrapCommand = WrapCommand;
632
709
  exports.textHelpers = selectionAndText;
633
- exports.unorderedListCommand = unorderedListCommand;
634
710
  exports.useTextAreaMarkdownEditor = useTextAreaMarkdownEditor;
635
711
  //# sourceMappingURL=index.js.map