markdown-actions 1.1.2 → 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
@@ -465,20 +465,20 @@ function undoOrderedListStyle(text) {
465
465
  }
466
466
  function undoUnorderedListStyle(text) {
467
467
  const lines = text.split("\n");
468
- const unorderedListPrefix = "- ";
469
- const shouldUndoUnorderedList = lines.every((line) => line.startsWith(unorderedListPrefix));
468
+ const unorderedListRegex = /^[-*+] /;
469
+ const shouldUndoUnorderedList = lines.every((line) => unorderedListRegex.test(line));
470
470
  let result = lines;
471
471
  if (shouldUndoUnorderedList) {
472
- result = lines.map((line) => line.slice(unorderedListPrefix.length));
472
+ result = lines.map((line) => line.replace(unorderedListRegex, ""));
473
473
  }
474
474
  return {
475
475
  text: result.join("\n"),
476
476
  processed: shouldUndoUnorderedList
477
477
  };
478
478
  }
479
- function makePrefix(index, unorderedList) {
479
+ function makePrefix(index, unorderedList, bulletMarker = "-") {
480
480
  if (unorderedList) {
481
- return "- ";
481
+ return `${bulletMarker} `;
482
482
  } else {
483
483
  return `${index + 1}. `;
484
484
  }
@@ -506,7 +506,7 @@ function listStyle(textarea, style) {
506
506
  const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
507
507
  const [undoResult, undoResultOppositeList, pristineText] = clearExistingListStyle(style, selectedText);
508
508
  const prefixedLines = pristineText.split("\n").map((value, index) => {
509
- return `${makePrefix(index, style.unorderedList)}${value}`;
509
+ return `${makePrefix(index, style.unorderedList, style.bulletMarker)}${value}`;
510
510
  });
511
511
  const totalPrefixLength = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => {
512
512
  return previousValue + makePrefix(currentIndex, style.unorderedList).length;
@@ -549,7 +549,7 @@ function applyListStyle(textarea, style) {
549
549
  adjustSelection: (isRemoving, selStart, selEnd, lineStart) => {
550
550
  const currentLine = textarea.value.slice(lineStart, textarea.selectionEnd);
551
551
  const orderedListRegex = /^\d+\.\s+/;
552
- const unorderedListRegex = /^- /;
552
+ const unorderedListRegex = /^[-*+] /;
553
553
  const hasOrderedList = orderedListRegex.test(currentLine);
554
554
  const hasUnorderedList = unorderedListRegex.test(currentLine);
555
555
  const isRemovingCurrent = style.orderedList && hasOrderedList || style.unorderedList && hasUnorderedList;
@@ -624,8 +624,8 @@ function getActiveFormats(textarea) {
624
624
  }
625
625
  lineStart += line.length + 1;
626
626
  }
627
- if (currentLine.startsWith("- ")) {
628
- if (currentLine.startsWith("- [ ] ") || currentLine.startsWith("- [x] ")) {
627
+ if (/^[-*+] /.test(currentLine)) {
628
+ if (/^[-*+] \[[ xX]\] /.test(currentLine)) {
629
629
  formats.push("task-list");
630
630
  } else {
631
631
  formats.push("bullet-list");
@@ -646,36 +646,30 @@ function getActiveFormats(textarea) {
646
646
  const lookBehind = Math.max(0, selectionStart - 10);
647
647
  const lookAhead = Math.min(value.length, selectionEnd + 10);
648
648
  const surrounding = value.slice(lookBehind, lookAhead);
649
- if (surrounding.includes("**")) {
650
- const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart);
651
- const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
652
- const lastOpenBold = beforeCursor.lastIndexOf("**");
653
- const nextCloseBold = afterCursor.indexOf("**");
654
- if (lastOpenBold !== -1 && nextCloseBold !== -1) {
655
- formats.push("bold");
656
- }
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");
657
653
  }
658
- if (surrounding.includes("_")) {
659
- const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart);
660
- const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
661
- const lastOpenItalic = beforeCursor.lastIndexOf("_");
662
- const nextCloseItalic = afterCursor.indexOf("_");
663
- if (lastOpenItalic !== -1 && nextCloseItalic !== -1) {
664
- formats.push("italic");
665
- }
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");
666
660
  }
667
661
  if (surrounding.includes("`")) {
668
- const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart);
669
- const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
670
- 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("`")) {
671
665
  formats.push("code");
672
666
  }
673
667
  }
674
668
  if (surrounding.includes("[") && surrounding.includes("]")) {
675
- const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart);
676
- const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100));
677
- const lastOpenBracket = beforeCursor.lastIndexOf("[");
678
- 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("]");
679
673
  if (lastOpenBracket !== -1 && nextCloseBracket !== -1) {
680
674
  const afterBracket = value.slice(selectionEnd + nextCloseBracket + 1, selectionEnd + nextCloseBracket + 10);
681
675
  if (afterBracket.startsWith("(")) {
@@ -723,23 +717,129 @@ function expandSelection(textarea, options = {}) {
723
717
  }
724
718
  }
725
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
+
726
828
  // src/index.js
727
- function toggleBold(textarea) {
829
+ function toggleBold(textarea, options = {}) {
728
830
  if (!textarea || textarea.disabled || textarea.readOnly)
729
831
  return;
730
832
  debugLog("toggleBold", "Starting");
731
833
  debugSelection(textarea, "Before");
732
- const style = mergeWithDefaults(FORMATS.bold);
733
- const result = blockStyle(textarea, style);
834
+ const result = inlineStyle(textarea, "bold", resolveMarker("bold", options.marker));
734
835
  debugResult(result);
735
836
  insertText(textarea, result);
736
837
  debugSelection(textarea, "After");
737
838
  }
738
- function toggleItalic(textarea) {
839
+ function toggleItalic(textarea, options = {}) {
739
840
  if (!textarea || textarea.disabled || textarea.readOnly)
740
841
  return;
741
- const style = mergeWithDefaults(FORMATS.italic);
742
- const result = blockStyle(textarea, style);
842
+ const result = inlineStyle(textarea, "italic", resolveMarker("italic", options.marker));
743
843
  insertText(textarea, result);
744
844
  }
745
845
  function toggleCode(textarea) {
@@ -771,10 +871,11 @@ function insertLink(textarea, options = {}) {
771
871
  const result = blockStyle(textarea, style);
772
872
  insertText(textarea, result);
773
873
  }
774
- function toggleBulletList(textarea) {
874
+ function toggleBulletList(textarea, options = {}) {
775
875
  if (!textarea || textarea.disabled || textarea.readOnly)
776
876
  return;
777
877
  const style = mergeWithDefaults(FORMATS.bulletList);
878
+ style.bulletMarker = resolveMarker("bullet", options.marker);
778
879
  applyListStyle(textarea, style);
779
880
  }
780
881
  function toggleNumberedList(textarea) {
@@ -957,6 +1058,7 @@ var src_default = {
957
1058
  hasFormat: hasFormat2,
958
1059
  expandSelection: expandSelection2,
959
1060
  applyCustomFormat,
1061
+ detectMarkers,
960
1062
  preserveSelection,
961
1063
  setUndoMethod,
962
1064
  setDebugMode,
@@ -965,6 +1067,7 @@ var src_default = {
965
1067
  export {
966
1068
  applyCustomFormat,
967
1069
  src_default as default,
1070
+ detectMarkers,
968
1071
  expandSelection2 as expandSelection,
969
1072
  getActiveFormats2 as getActiveFormats,
970
1073
  getDebugMode,