react-headless-mde 2.0.3 → 3.0.0

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