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 +9 -1
- package/dist/markdown-actions.esm.js +142 -39
- package/dist/markdown-actions.esm.js.map +4 -4
- package/dist/markdown-actions.js +142 -39
- package/dist/markdown-actions.js.map +4 -4
- package/dist/markdown-actions.min.js +22 -23
- package/package.json +12 -3
- package/src/core/detection.js +15 -19
- package/src/core/markers.js +34 -0
- package/src/index.js +20 -7
- package/src/operations/inline.js +101 -0
- package/src/operations/list.js +7 -7
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
|
|
469
|
-
const shouldUndoUnorderedList = lines.every((line) =>
|
|
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.
|
|
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
|
|
628
|
-
if (
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
|
669
|
-
const
|
|
670
|
-
if (
|
|
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
|
|
676
|
-
const
|
|
677
|
-
const lastOpenBracket =
|
|
678
|
-
const nextCloseBracket =
|
|
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
|
|
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
|
|
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,
|