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.
@@ -40,8 +40,8 @@ export function getActiveFormats(textarea) {
40
40
  }
41
41
 
42
42
  // Check line-based formats
43
- if (currentLine.startsWith('- ')) {
44
- if (currentLine.startsWith('- [ ] ') || currentLine.startsWith('- [x] ')) {
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
- if (surrounding.includes('**')) {
70
- const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart)
71
- const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100))
72
- const lastOpenBold = beforeCursor.lastIndexOf('**')
73
- const nextCloseBold = afterCursor.indexOf('**')
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
- if (surrounding.includes('_')) {
81
- const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart)
82
- const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100))
83
- const lastOpenItalic = beforeCursor.lastIndexOf('_')
84
- const nextCloseItalic = afterCursor.indexOf('_')
85
- if (lastOpenItalic !== -1 && nextCloseItalic !== -1) {
86
- formats.push('italic')
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
@@ -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 style = mergeWithDefaults(FORMATS.bold)
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 style = mergeWithDefaults(FORMATS.italic)
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
+ }
@@ -28,11 +28,11 @@ function undoOrderedListStyle(text) {
28
28
  */
29
29
  function undoUnorderedListStyle(text) {
30
30
  const lines = text.split('\n')
31
- const unorderedListPrefix = '- '
32
- const shouldUndoUnorderedList = lines.every(line => line.startsWith(unorderedListPrefix))
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.slice(unorderedListPrefix.length))
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)