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/src/core/detection.js
CHANGED
|
@@ -40,8 +40,8 @@ export function getActiveFormats(textarea) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Check line-based formats
|
|
43
|
-
if (currentLine
|
|
44
|
-
if (
|
|
43
|
+
if (/^[-*+] /.test(currentLine)) {
|
|
44
|
+
if (/^[-*+] \[[ xX]\] /.test(currentLine)) {
|
|
45
45
|
formats.push('task-list')
|
|
46
46
|
} else {
|
|
47
47
|
formats.push('bullet-list')
|
|
@@ -66,25 +66,21 @@ export function getActiveFormats(textarea) {
|
|
|
66
66
|
const surrounding = value.slice(lookBehind, lookAhead)
|
|
67
67
|
|
|
68
68
|
// Check for bold
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (lastOpenBold !== -1 && nextCloseBold !== -1) {
|
|
75
|
-
formats.push('bold')
|
|
76
|
-
}
|
|
69
|
+
const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart)
|
|
70
|
+
const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100))
|
|
71
|
+
if (['**', '__'].some(marker => surrounding.includes(marker) &&
|
|
72
|
+
beforeCursor.lastIndexOf(marker) !== -1 && afterCursor.indexOf(marker) !== -1)) {
|
|
73
|
+
formats.push('bold')
|
|
77
74
|
}
|
|
78
75
|
|
|
79
|
-
// Check for italic
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
76
|
+
// Check for italic: a single _ or * on each side, not part of __ or **
|
|
77
|
+
const singleMarker = ch => {
|
|
78
|
+
const c = ch === '*' ? '\\*' : ch
|
|
79
|
+
return new RegExp(`(^|[^${c}])${c}[^${c}]*$`).test(beforeCursor) &&
|
|
80
|
+
new RegExp(`^[^${c}]*${c}(?!${c})`).test(afterCursor)
|
|
81
|
+
}
|
|
82
|
+
if ((surrounding.includes('_') && singleMarker('_')) || (surrounding.includes('*') && singleMarker('*'))) {
|
|
83
|
+
formats.push('italic')
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
// Check for code
|
package/src/core/insertion.js
CHANGED
|
@@ -50,8 +50,10 @@ export function insertText(textarea, { text, selectionStart, selectionEnd }) {
|
|
|
50
50
|
textarea.contentEditable = 'true'
|
|
51
51
|
try {
|
|
52
52
|
canInsertText = document.execCommand('insertText', false, text)
|
|
53
|
+
if (debugMode) console.log('execCommand returned:', canInsertText, 'for text with', text.split('\n').length, 'lines');
|
|
53
54
|
} catch (error) {
|
|
54
55
|
canInsertText = false
|
|
56
|
+
if (debugMode) console.log('execCommand threw error:', error);
|
|
55
57
|
}
|
|
56
58
|
textarea.contentEditable = 'false'
|
|
57
59
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown spellings that mean the same thing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const MARKERS = {
|
|
6
|
+
bold: ['**', '__'],
|
|
7
|
+
italic: ['_', '*'],
|
|
8
|
+
bullet: ['-', '*', '+']
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Return the marker if it is a standard spelling for this format, otherwise the default
|
|
13
|
+
*/
|
|
14
|
+
export function resolveMarker(kind, marker) {
|
|
15
|
+
return MARKERS[kind].indexOf(marker) !== -1 ? marker : MARKERS[kind][0]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DETECT = {
|
|
19
|
+
bullet: /^[ \t]*([-*+])[ \t]+\S/m,
|
|
20
|
+
bold: /(\*\*|__)(?=[^\s*_])[^\n]*?[^\s*_]\1/,
|
|
21
|
+
italic: /(?:^|[^*_\w])([*_])(?=[^\s*_])[^\n*_]*?[^\s*_]\1(?![*_\w])/
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find the spelling a document already uses for each format, or null when it uses none
|
|
26
|
+
*/
|
|
27
|
+
export function detectMarkers(text) {
|
|
28
|
+
const found = {}
|
|
29
|
+
Object.keys(DETECT).forEach(kind => {
|
|
30
|
+
const match = DETECT[kind].exec(text || '')
|
|
31
|
+
found[kind] = match ? match[1] : null
|
|
32
|
+
})
|
|
33
|
+
return found
|
|
34
|
+
}
|
package/src/index.js
CHANGED
|
@@ -10,18 +10,21 @@ import { blockStyle, multilineStyle } from './operations/block.js'
|
|
|
10
10
|
import { applyListStyle } from './operations/list.js'
|
|
11
11
|
import { getActiveFormats as getActive, hasFormat as has, expandSelection as expand } from './core/detection.js'
|
|
12
12
|
import { debugLog, debugSelection, debugResult, setDebugMode, getDebugMode } from './debug.js'
|
|
13
|
+
import { inlineStyle } from './operations/inline.js'
|
|
14
|
+
import { resolveMarker, detectMarkers } from './core/markers.js'
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Toggle bold formatting
|
|
18
|
+
* @param {Object} [options]
|
|
19
|
+
* @param {string} [options.marker] - '**' (default) or '__'
|
|
16
20
|
*/
|
|
17
|
-
export function toggleBold(textarea) {
|
|
21
|
+
export function toggleBold(textarea, options = {}) {
|
|
18
22
|
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
19
23
|
|
|
20
24
|
debugLog('toggleBold', 'Starting');
|
|
21
25
|
debugSelection(textarea, 'Before');
|
|
22
26
|
|
|
23
|
-
const
|
|
24
|
-
const result = blockStyle(textarea, style)
|
|
27
|
+
const result = inlineStyle(textarea, 'bold', resolveMarker('bold', options.marker))
|
|
25
28
|
|
|
26
29
|
debugResult(result);
|
|
27
30
|
|
|
@@ -32,11 +35,12 @@ export function toggleBold(textarea) {
|
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* Toggle italic formatting
|
|
38
|
+
* @param {Object} [options]
|
|
39
|
+
* @param {string} [options.marker] - '_' (default) or '*'
|
|
35
40
|
*/
|
|
36
|
-
export function toggleItalic(textarea) {
|
|
41
|
+
export function toggleItalic(textarea, options = {}) {
|
|
37
42
|
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
38
|
-
const
|
|
39
|
-
const result = blockStyle(textarea, style)
|
|
43
|
+
const result = inlineStyle(textarea, 'italic', resolveMarker('italic', options.marker))
|
|
40
44
|
insertText(textarea, result)
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -90,10 +94,13 @@ export function insertLink(textarea, options = {}) {
|
|
|
90
94
|
|
|
91
95
|
/**
|
|
92
96
|
* Toggle bullet list formatting
|
|
97
|
+
* @param {Object} [options]
|
|
98
|
+
* @param {string} [options.marker] - '-' (default), '*' or '+'
|
|
93
99
|
*/
|
|
94
|
-
export function toggleBulletList(textarea) {
|
|
100
|
+
export function toggleBulletList(textarea, options = {}) {
|
|
95
101
|
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
96
102
|
const style = mergeWithDefaults(FORMATS.bulletList)
|
|
103
|
+
style.bulletMarker = resolveMarker('bullet', options.marker)
|
|
97
104
|
applyListStyle(textarea, style)
|
|
98
105
|
}
|
|
99
106
|
|
|
@@ -346,6 +353,11 @@ export function applyCustomFormat(textarea, format) {
|
|
|
346
353
|
*/
|
|
347
354
|
export { preserveSelection }
|
|
348
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Standard spellings per format, and the ones a document already uses
|
|
358
|
+
*/
|
|
359
|
+
export { detectMarkers }
|
|
360
|
+
|
|
349
361
|
/**
|
|
350
362
|
* Configure undo method
|
|
351
363
|
*/
|
|
@@ -376,6 +388,7 @@ export default {
|
|
|
376
388
|
hasFormat,
|
|
377
389
|
expandSelection,
|
|
378
390
|
applyCustomFormat,
|
|
391
|
+
detectMarkers,
|
|
379
392
|
preserveSelection,
|
|
380
393
|
setUndoMethod,
|
|
381
394
|
setDebugMode,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bold and italic toggles that understand both spellings (** and __, * and _)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { wordSelectionStart, wordSelectionEnd } from '../core/selection.js'
|
|
6
|
+
|
|
7
|
+
const isEmphasis = ch => ch === '*' || ch === '_'
|
|
8
|
+
const OPENING_PUNCTUATION = /[(\[{"'“‘]/
|
|
9
|
+
const CLOSING_PUNCTUATION = /[.,;:!?)\]}"'”’]/
|
|
10
|
+
|
|
11
|
+
// A run of n emphasis characters carries italic when n is odd, and bold when n is at least 2
|
|
12
|
+
function carries(kind, n) {
|
|
13
|
+
return kind === 'bold' ? n >= 2 : n % 2 === 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runLength(text, ch, fromEnd) {
|
|
17
|
+
let n = 0
|
|
18
|
+
while (n < text.length && text[fromEnd ? text.length - 1 - n : n] === ch) n++
|
|
19
|
+
return n
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Find the markers that already wrap value[start, end] as this format
|
|
24
|
+
*/
|
|
25
|
+
export function findWrap(value, start, end, kind) {
|
|
26
|
+
const size = kind === 'bold' ? 2 : 1
|
|
27
|
+
let open = start
|
|
28
|
+
while (open > 0 && isEmphasis(value[open - 1])) open--
|
|
29
|
+
let close = end
|
|
30
|
+
while (close < value.length && isEmphasis(value[close])) close++
|
|
31
|
+
const before = value.slice(open, start)
|
|
32
|
+
const after = value.slice(end, close)
|
|
33
|
+
|
|
34
|
+
for (const ch of ['*', '_']) {
|
|
35
|
+
if (carries(kind, runLength(before, ch, true)) && carries(kind, runLength(after, ch, false))) {
|
|
36
|
+
return { open: start - size, close: end, size }
|
|
37
|
+
}
|
|
38
|
+
const outerBefore = runLength(before, ch, false)
|
|
39
|
+
if (outerBefore < before.length && carries(kind, outerBefore) && carries(kind, runLength(after, ch, true))) {
|
|
40
|
+
return { open, close: close - size, size }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The text an inline toggle acts on: the selection, or the word at the caret,
|
|
48
|
+
* without surrounding punctuation or emphasis markers
|
|
49
|
+
*/
|
|
50
|
+
function targetRange(value, selectionStart, selectionEnd) {
|
|
51
|
+
let start = selectionStart
|
|
52
|
+
let end = selectionEnd
|
|
53
|
+
if (start === end) {
|
|
54
|
+
start = wordSelectionStart(value, start)
|
|
55
|
+
end = wordSelectionEnd(value, end, false)
|
|
56
|
+
while (start < end && OPENING_PUNCTUATION.test(value[start])) start++
|
|
57
|
+
while (end > start && CLOSING_PUNCTUATION.test(value[end - 1])) end--
|
|
58
|
+
}
|
|
59
|
+
while (start < end && /\s/.test(value[start])) start++
|
|
60
|
+
while (end > start && /\s/.test(value[end - 1])) end--
|
|
61
|
+
while (start < end && isEmphasis(value[start])) start++
|
|
62
|
+
while (end > start && isEmphasis(value[end - 1])) end--
|
|
63
|
+
return { start, end }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const clamp = (n, low, high) => Math.min(Math.max(n, low), high)
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Toggle bold or italic on the selection or the word at the caret.
|
|
70
|
+
* Removes either spelling, and adds the given marker.
|
|
71
|
+
*/
|
|
72
|
+
export function inlineStyle(textarea, kind, marker) {
|
|
73
|
+
const value = textarea.value
|
|
74
|
+
const caretStart = textarea.selectionStart
|
|
75
|
+
const caretEnd = textarea.selectionEnd
|
|
76
|
+
const hadSelection = caretStart !== caretEnd
|
|
77
|
+
const { start, end } = targetRange(value, caretStart, caretEnd)
|
|
78
|
+
|
|
79
|
+
const wrap = start < end ? findWrap(value, start, end, kind) : null
|
|
80
|
+
if (wrap) {
|
|
81
|
+
textarea.selectionStart = wrap.open
|
|
82
|
+
textarea.selectionEnd = wrap.close + wrap.size
|
|
83
|
+
const newStart = start - wrap.size
|
|
84
|
+
const newEnd = end - wrap.size
|
|
85
|
+
const caret = clamp(caretStart - wrap.size, newStart, newEnd)
|
|
86
|
+
return {
|
|
87
|
+
text: value.slice(wrap.open + wrap.size, wrap.close),
|
|
88
|
+
selectionStart: hadSelection ? newStart : caret,
|
|
89
|
+
selectionEnd: hadSelection ? newEnd : caret
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
textarea.selectionStart = start
|
|
94
|
+
textarea.selectionEnd = end
|
|
95
|
+
const caret = clamp(caretStart, start, end) + marker.length
|
|
96
|
+
return {
|
|
97
|
+
text: marker + value.slice(start, end) + marker,
|
|
98
|
+
selectionStart: hadSelection ? start + marker.length : caret,
|
|
99
|
+
selectionEnd: hadSelection ? end + marker.length : caret
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/operations/list.js
CHANGED
|
@@ -28,11 +28,11 @@ function undoOrderedListStyle(text) {
|
|
|
28
28
|
*/
|
|
29
29
|
function undoUnorderedListStyle(text) {
|
|
30
30
|
const lines = text.split('\n')
|
|
31
|
-
const
|
|
32
|
-
const shouldUndoUnorderedList = lines.every(line =>
|
|
31
|
+
const unorderedListRegex = /^[-*+] /
|
|
32
|
+
const shouldUndoUnorderedList = lines.every(line => unorderedListRegex.test(line))
|
|
33
33
|
let result = lines
|
|
34
34
|
if (shouldUndoUnorderedList) {
|
|
35
|
-
result = lines.map(line => line.
|
|
35
|
+
result = lines.map(line => line.replace(unorderedListRegex, ''))
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
return {
|
|
@@ -44,9 +44,9 @@ function undoUnorderedListStyle(text) {
|
|
|
44
44
|
/**
|
|
45
45
|
* Make prefix for list item
|
|
46
46
|
*/
|
|
47
|
-
function makePrefix(index, unorderedList) {
|
|
47
|
+
function makePrefix(index, unorderedList, bulletMarker = '-') {
|
|
48
48
|
if (unorderedList) {
|
|
49
|
-
return
|
|
49
|
+
return `${bulletMarker} `
|
|
50
50
|
} else {
|
|
51
51
|
return `${index + 1}. `
|
|
52
52
|
}
|
|
@@ -90,7 +90,7 @@ export function listStyle(textarea, style) {
|
|
|
90
90
|
const [undoResult, undoResultOppositeList, pristineText] = clearExistingListStyle(style, selectedText)
|
|
91
91
|
|
|
92
92
|
const prefixedLines = pristineText.split('\n').map((value, index) => {
|
|
93
|
-
return `${makePrefix(index, style.unorderedList)}${value}`
|
|
93
|
+
return `${makePrefix(index, style.unorderedList, style.bulletMarker)}${value}`
|
|
94
94
|
})
|
|
95
95
|
|
|
96
96
|
const totalPrefixLength = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => {
|
|
@@ -149,7 +149,7 @@ export function applyListStyle(textarea, style) {
|
|
|
149
149
|
// Get the current line to check if we're removing
|
|
150
150
|
const currentLine = textarea.value.slice(lineStart, textarea.selectionEnd)
|
|
151
151
|
const orderedListRegex = /^\d+\.\s+/
|
|
152
|
-
const unorderedListRegex =
|
|
152
|
+
const unorderedListRegex = /^[-*+] /
|
|
153
153
|
|
|
154
154
|
// Check if we're removing a list
|
|
155
155
|
const hasOrderedList = orderedListRegex.test(currentLine)
|