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 +9 -1
- package/dist/markdown-actions.esm.js +146 -39
- package/dist/markdown-actions.esm.js.map +4 -4
- package/dist/markdown-actions.js +146 -39
- package/dist/markdown-actions.js.map +4 -4
- package/dist/markdown-actions.min.js +22 -22
- package/package.json +12 -3
- package/src/core/detection.js +15 -19
- package/src/core/insertion.js +2 -0
- 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
|
|
@@ -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
|
|
465
|
-
const shouldUndoUnorderedList = lines.every((line) =>
|
|
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.
|
|
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
|
|
624
|
-
if (
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
|
665
|
-
const
|
|
666
|
-
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("`")) {
|
|
667
665
|
formats.push("code");
|
|
668
666
|
}
|
|
669
667
|
}
|
|
670
668
|
if (surrounding.includes("[") && surrounding.includes("]")) {
|
|
671
|
-
const
|
|
672
|
-
const
|
|
673
|
-
const lastOpenBracket =
|
|
674
|
-
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("]");
|
|
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
|
|
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
|
|
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,
|