markdown-actions 1.1.1 → 1.2.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.
package/README.md CHANGED
@@ -71,6 +71,9 @@ hasFormat(textarea, 'bold') // Returns: true/false
71
71
  // Expand selection to word or line boundaries
72
72
  expandSelection(textarea, { toWord: true })
73
73
 
74
+ // Find the spelling a document already uses (null when it uses none)
75
+ detectMarkers('Hi *there*\n\n+ one') // Returns: { italic: '*', bold: null, bullet: '+' }
76
+
74
77
  // Apply custom markdown format
75
78
  applyCustomFormat(textarea, {
76
79
  prefix: '==',
@@ -82,6 +85,11 @@ applyCustomFormat(textarea, {
82
85
  ### Options
83
86
 
84
87
  ```javascript
88
+ // Choose the spelling. Toggling off removes either spelling.
89
+ toggleItalic(textarea, { marker: '*' }) // '_' (default) or '*'
90
+ toggleBold(textarea, { marker: '__' }) // '**' (default) or '__'
91
+ toggleBulletList(textarea, { marker: '*' }) // '-' (default), '*' or '+'
92
+
85
93
  // Link options
86
94
  insertLink(textarea, {
87
95
  url: 'https://example.com', // Pre-fill URL
@@ -294,7 +302,7 @@ This careful selection management creates a professional editing experience:
294
302
 
295
303
  ## 🧪 Testing
296
304
 
297
- Open `test.html` in a browser to run the comprehensive test suite with 38 test cases covering:
305
+ Run `npm test` for the Node tests (spellings, toggling, active formats). Open `test.html` in a browser to run the comprehensive test suite with 38 test cases covering:
298
306
 
299
307
  - All formatting functions
300
308
  - Edge cases and boundary conditions
@@ -152,8 +152,12 @@ function insertText(textarea, { text, selectionStart, selectionEnd }) {
152
152
  textarea.contentEditable = "true";
153
153
  try {
154
154
  canInsertText = document.execCommand("insertText", false, text);
155
+ if (debugMode2)
156
+ console.log("execCommand returned:", canInsertText, "for text with", text.split("\n").length, "lines");
155
157
  } catch (error) {
156
158
  canInsertText = false;
159
+ if (debugMode2)
160
+ console.log("execCommand threw error:", error);
157
161
  }
158
162
  textarea.contentEditable = "false";
159
163
  }
@@ -461,20 +465,20 @@ function undoOrderedListStyle(text) {
461
465
  }
462
466
  function undoUnorderedListStyle(text) {
463
467
  const lines = text.split("\n");
464
- const unorderedListPrefix = "- ";
465
- const shouldUndoUnorderedList = lines.every((line) => line.startsWith(unorderedListPrefix));
468
+ const unorderedListRegex = /^[-*+] /;
469
+ const shouldUndoUnorderedList = lines.every((line) => unorderedListRegex.test(line));
466
470
  let result = lines;
467
471
  if (shouldUndoUnorderedList) {
468
- result = lines.map((line) => line.slice(unorderedListPrefix.length));
472
+ result = lines.map((line) => line.replace(unorderedListRegex, ""));
469
473
  }
470
474
  return {
471
475
  text: result.join("\n"),
472
476
  processed: shouldUndoUnorderedList
473
477
  };
474
478
  }
475
- function makePrefix(index, unorderedList) {
479
+ function makePrefix(index, unorderedList, bulletMarker = "-") {
476
480
  if (unorderedList) {
477
- return "- ";
481
+ return `${bulletMarker} `;
478
482
  } else {
479
483
  return `${index + 1}. `;
480
484
  }
@@ -502,7 +506,7 @@ function listStyle(textarea, style) {
502
506
  const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
503
507
  const [undoResult, undoResultOppositeList, pristineText] = clearExistingListStyle(style, selectedText);
504
508
  const prefixedLines = pristineText.split("\n").map((value, index) => {
505
- return `${makePrefix(index, style.unorderedList)}${value}`;
509
+ return `${makePrefix(index, style.unorderedList, style.bulletMarker)}${value}`;
506
510
  });
507
511
  const totalPrefixLength = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => {
508
512
  return previousValue + makePrefix(currentIndex, style.unorderedList).length;
@@ -545,7 +549,7 @@ function applyListStyle(textarea, style) {
545
549
  adjustSelection: (isRemoving, selStart, selEnd, lineStart) => {
546
550
  const currentLine = textarea.value.slice(lineStart, textarea.selectionEnd);
547
551
  const orderedListRegex = /^\d+\.\s+/;
548
- const unorderedListRegex = /^- /;
552
+ const unorderedListRegex = /^[-*+] /;
549
553
  const hasOrderedList = orderedListRegex.test(currentLine);
550
554
  const hasUnorderedList = unorderedListRegex.test(currentLine);
551
555
  const isRemovingCurrent = style.orderedList && hasOrderedList || style.unorderedList && hasUnorderedList;
@@ -620,8 +624,8 @@ function getActiveFormats(textarea) {
620
624
  }
621
625
  lineStart += line.length + 1;
622
626
  }
623
- if (currentLine.startsWith("- ")) {
624
- if (currentLine.startsWith("- [ ] ") || currentLine.startsWith("- [x] ")) {
627
+ if (/^[-*+] /.test(currentLine)) {
628
+ if (/^[-*+] \[[ xX]\] /.test(currentLine)) {
625
629
  formats.push("task-list");
626
630
  } else {
627
631
  formats.push("bullet-list");
@@ -642,36 +646,30 @@ function getActiveFormats(textarea) {
642
646
  const lookBehind = Math.max(0, selectionStart - 10);
643
647
  const lookAhead = Math.min(value.length, selectionEnd + 10);
644
648
  const surrounding = value.slice(lookBehind, lookAhead);
645
- if (surrounding.includes("**")) {
646
- const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart);
647
- const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
648
- const lastOpenBold = beforeCursor.lastIndexOf("**");
649
- const nextCloseBold = afterCursor.indexOf("**");
650
- if (lastOpenBold !== -1 && nextCloseBold !== -1) {
651
- formats.push("bold");
652
- }
649
+ const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart);
650
+ const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
651
+ if (["**", "__"].some((marker) => surrounding.includes(marker) && beforeCursor.lastIndexOf(marker) !== -1 && afterCursor.indexOf(marker) !== -1)) {
652
+ formats.push("bold");
653
653
  }
654
- if (surrounding.includes("_")) {
655
- const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart);
656
- const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
657
- const lastOpenItalic = beforeCursor.lastIndexOf("_");
658
- const nextCloseItalic = afterCursor.indexOf("_");
659
- if (lastOpenItalic !== -1 && nextCloseItalic !== -1) {
660
- formats.push("italic");
661
- }
654
+ const singleMarker = (ch) => {
655
+ const c = ch === "*" ? "\\*" : ch;
656
+ return new RegExp(`(^|[^${c}])${c}[^${c}]*$`).test(beforeCursor) && new RegExp(`^[^${c}]*${c}(?!${c})`).test(afterCursor);
657
+ };
658
+ if (surrounding.includes("_") && singleMarker("_") || surrounding.includes("*") && singleMarker("*")) {
659
+ formats.push("italic");
662
660
  }
663
661
  if (surrounding.includes("`")) {
664
- const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart);
665
- const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
666
- if (beforeCursor.includes("`") && afterCursor.includes("`")) {
662
+ const beforeCursor2 = value.slice(Math.max(0, selectionStart - 100), selectionStart);
663
+ const afterCursor2 = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
664
+ if (beforeCursor2.includes("`") && afterCursor2.includes("`")) {
667
665
  formats.push("code");
668
666
  }
669
667
  }
670
668
  if (surrounding.includes("[") && surrounding.includes("]")) {
671
- const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart);
672
- const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
673
- const lastOpenBracket = beforeCursor.lastIndexOf("[");
674
- const nextCloseBracket = afterCursor.indexOf("]");
669
+ const beforeCursor2 = value.slice(Math.max(0, selectionStart - 100), selectionStart);
670
+ const afterCursor2 = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
671
+ const lastOpenBracket = beforeCursor2.lastIndexOf("[");
672
+ const nextCloseBracket = afterCursor2.indexOf("]");
675
673
  if (lastOpenBracket !== -1 && nextCloseBracket !== -1) {
676
674
  const afterBracket = value.slice(selectionEnd + nextCloseBracket + 1, selectionEnd + nextCloseBracket + 10);
677
675
  if (afterBracket.startsWith("(")) {
@@ -719,23 +717,129 @@ function expandSelection(textarea, options = {}) {
719
717
  }
720
718
  }
721
719
 
720
+ // src/operations/inline.js
721
+ var isEmphasis = (ch) => ch === "*" || ch === "_";
722
+ var OPENING_PUNCTUATION = /[(\[{"'“‘]/;
723
+ var CLOSING_PUNCTUATION = /[.,;:!?)\]}"'”’]/;
724
+ function carries(kind, n) {
725
+ return kind === "bold" ? n >= 2 : n % 2 === 1;
726
+ }
727
+ function runLength(text, ch, fromEnd) {
728
+ let n = 0;
729
+ while (n < text.length && text[fromEnd ? text.length - 1 - n : n] === ch)
730
+ n++;
731
+ return n;
732
+ }
733
+ function findWrap(value, start, end, kind) {
734
+ const size = kind === "bold" ? 2 : 1;
735
+ let open = start;
736
+ while (open > 0 && isEmphasis(value[open - 1]))
737
+ open--;
738
+ let close = end;
739
+ while (close < value.length && isEmphasis(value[close]))
740
+ close++;
741
+ const before = value.slice(open, start);
742
+ const after = value.slice(end, close);
743
+ for (const ch of ["*", "_"]) {
744
+ if (carries(kind, runLength(before, ch, true)) && carries(kind, runLength(after, ch, false))) {
745
+ return { open: start - size, close: end, size };
746
+ }
747
+ const outerBefore = runLength(before, ch, false);
748
+ if (outerBefore < before.length && carries(kind, outerBefore) && carries(kind, runLength(after, ch, true))) {
749
+ return { open, close: close - size, size };
750
+ }
751
+ }
752
+ return null;
753
+ }
754
+ function targetRange(value, selectionStart, selectionEnd) {
755
+ let start = selectionStart;
756
+ let end = selectionEnd;
757
+ if (start === end) {
758
+ start = wordSelectionStart(value, start);
759
+ end = wordSelectionEnd(value, end, false);
760
+ while (start < end && OPENING_PUNCTUATION.test(value[start]))
761
+ start++;
762
+ while (end > start && CLOSING_PUNCTUATION.test(value[end - 1]))
763
+ end--;
764
+ }
765
+ while (start < end && /\s/.test(value[start]))
766
+ start++;
767
+ while (end > start && /\s/.test(value[end - 1]))
768
+ end--;
769
+ while (start < end && isEmphasis(value[start]))
770
+ start++;
771
+ while (end > start && isEmphasis(value[end - 1]))
772
+ end--;
773
+ return { start, end };
774
+ }
775
+ var clamp = (n, low, high) => Math.min(Math.max(n, low), high);
776
+ function inlineStyle(textarea, kind, marker) {
777
+ const value = textarea.value;
778
+ const caretStart = textarea.selectionStart;
779
+ const caretEnd = textarea.selectionEnd;
780
+ const hadSelection = caretStart !== caretEnd;
781
+ const { start, end } = targetRange(value, caretStart, caretEnd);
782
+ const wrap = start < end ? findWrap(value, start, end, kind) : null;
783
+ if (wrap) {
784
+ textarea.selectionStart = wrap.open;
785
+ textarea.selectionEnd = wrap.close + wrap.size;
786
+ const newStart = start - wrap.size;
787
+ const newEnd = end - wrap.size;
788
+ const caret2 = clamp(caretStart - wrap.size, newStart, newEnd);
789
+ return {
790
+ text: value.slice(wrap.open + wrap.size, wrap.close),
791
+ selectionStart: hadSelection ? newStart : caret2,
792
+ selectionEnd: hadSelection ? newEnd : caret2
793
+ };
794
+ }
795
+ textarea.selectionStart = start;
796
+ textarea.selectionEnd = end;
797
+ const caret = clamp(caretStart, start, end) + marker.length;
798
+ return {
799
+ text: marker + value.slice(start, end) + marker,
800
+ selectionStart: hadSelection ? start + marker.length : caret,
801
+ selectionEnd: hadSelection ? end + marker.length : caret
802
+ };
803
+ }
804
+
805
+ // src/core/markers.js
806
+ var MARKERS = {
807
+ bold: ["**", "__"],
808
+ italic: ["_", "*"],
809
+ bullet: ["-", "*", "+"]
810
+ };
811
+ function resolveMarker(kind, marker) {
812
+ return MARKERS[kind].indexOf(marker) !== -1 ? marker : MARKERS[kind][0];
813
+ }
814
+ var DETECT = {
815
+ bullet: /^[ \t]*([-*+])[ \t]+\S/m,
816
+ bold: /(\*\*|__)(?=[^\s*_])[^\n]*?[^\s*_]\1/,
817
+ italic: /(?:^|[^*_\w])([*_])(?=[^\s*_])[^\n*_]*?[^\s*_]\1(?![*_\w])/
818
+ };
819
+ function detectMarkers(text) {
820
+ const found = {};
821
+ Object.keys(DETECT).forEach((kind) => {
822
+ const match = DETECT[kind].exec(text || "");
823
+ found[kind] = match ? match[1] : null;
824
+ });
825
+ return found;
826
+ }
827
+
722
828
  // src/index.js
723
- function toggleBold(textarea) {
829
+ function toggleBold(textarea, options = {}) {
724
830
  if (!textarea || textarea.disabled || textarea.readOnly)
725
831
  return;
726
832
  debugLog("toggleBold", "Starting");
727
833
  debugSelection(textarea, "Before");
728
- const style = mergeWithDefaults(FORMATS.bold);
729
- const result = blockStyle(textarea, style);
834
+ const result = inlineStyle(textarea, "bold", resolveMarker("bold", options.marker));
730
835
  debugResult(result);
731
836
  insertText(textarea, result);
732
837
  debugSelection(textarea, "After");
733
838
  }
734
- function toggleItalic(textarea) {
839
+ function toggleItalic(textarea, options = {}) {
735
840
  if (!textarea || textarea.disabled || textarea.readOnly)
736
841
  return;
737
- const style = mergeWithDefaults(FORMATS.italic);
738
- const result = blockStyle(textarea, style);
842
+ const result = inlineStyle(textarea, "italic", resolveMarker("italic", options.marker));
739
843
  insertText(textarea, result);
740
844
  }
741
845
  function toggleCode(textarea) {
@@ -767,10 +871,11 @@ function insertLink(textarea, options = {}) {
767
871
  const result = blockStyle(textarea, style);
768
872
  insertText(textarea, result);
769
873
  }
770
- function toggleBulletList(textarea) {
874
+ function toggleBulletList(textarea, options = {}) {
771
875
  if (!textarea || textarea.disabled || textarea.readOnly)
772
876
  return;
773
877
  const style = mergeWithDefaults(FORMATS.bulletList);
878
+ style.bulletMarker = resolveMarker("bullet", options.marker);
774
879
  applyListStyle(textarea, style);
775
880
  }
776
881
  function toggleNumberedList(textarea) {
@@ -953,6 +1058,7 @@ var src_default = {
953
1058
  hasFormat: hasFormat2,
954
1059
  expandSelection: expandSelection2,
955
1060
  applyCustomFormat,
1061
+ detectMarkers,
956
1062
  preserveSelection,
957
1063
  setUndoMethod,
958
1064
  setDebugMode,
@@ -961,6 +1067,7 @@ var src_default = {
961
1067
  export {
962
1068
  applyCustomFormat,
963
1069
  src_default as default,
1070
+ detectMarkers,
964
1071
  expandSelection2 as expandSelection,
965
1072
  getActiveFormats2 as getActiveFormats,
966
1073
  getDebugMode,