markdown-actions 1.0.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/LICENSE +33 -0
- package/README.md +379 -0
- package/dist/markdown-actions.esm.js +625 -0
- package/dist/markdown-actions.esm.js.map +7 -0
- package/dist/markdown-actions.js +646 -0
- package/dist/markdown-actions.js.map +7 -0
- package/dist/markdown-actions.min.js +19 -0
- package/package.json +45 -0
- package/src/core/detection.js +169 -0
- package/src/core/formats.js +76 -0
- package/src/core/insertion-new.js +66 -0
- package/src/core/insertion.js +79 -0
- package/src/core/selection-new.js +137 -0
- package/src/core/selection.js +117 -0
- package/src/core/style-args.js +33 -0
- package/src/core/style-text.js +30 -0
- package/src/index-new.js +260 -0
- package/src/index.js +229 -0
- package/src/operations/block-new.js +109 -0
- package/src/operations/block.js +139 -0
- package/src/operations/list-new.js +137 -0
- package/src/operations/list.js +152 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown-actions - Lightweight markdown toolbar functionality
|
|
3
|
+
* Based on GitHub's markdown-toolbar-element
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FORMATS, mergeWithDefaults } from './core/formats.js'
|
|
7
|
+
import { insertText, setUndoMethod } from './core/insertion.js'
|
|
8
|
+
import { preserveSelection, isMultipleLines } from './core/selection.js'
|
|
9
|
+
import { blockStyle, multilineStyle } from './operations/block.js'
|
|
10
|
+
import { applyListStyle } from './operations/list.js'
|
|
11
|
+
import { getActiveFormats as getActive, hasFormat as has, expandSelection as expand } from './core/detection.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Toggle bold formatting
|
|
15
|
+
*/
|
|
16
|
+
export function toggleBold(textarea) {
|
|
17
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
18
|
+
const style = mergeWithDefaults(FORMATS.bold)
|
|
19
|
+
const result = blockStyle(textarea, style)
|
|
20
|
+
insertText(textarea, result)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Toggle italic formatting
|
|
25
|
+
*/
|
|
26
|
+
export function toggleItalic(textarea) {
|
|
27
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
28
|
+
const style = mergeWithDefaults(FORMATS.italic)
|
|
29
|
+
const result = blockStyle(textarea, style)
|
|
30
|
+
insertText(textarea, result)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Toggle code formatting
|
|
35
|
+
*/
|
|
36
|
+
export function toggleCode(textarea) {
|
|
37
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
38
|
+
const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
39
|
+
const style = mergeWithDefaults(FORMATS.code)
|
|
40
|
+
|
|
41
|
+
// Use block style for single line, multiline style for multiple lines
|
|
42
|
+
let result
|
|
43
|
+
if (isMultipleLines(selectedText)) {
|
|
44
|
+
// For multiline, we want code blocks
|
|
45
|
+
const blockStyle = {
|
|
46
|
+
prefix: '```\n',
|
|
47
|
+
suffix: '\n```',
|
|
48
|
+
surroundWithNewlines: true
|
|
49
|
+
}
|
|
50
|
+
result = multilineStyle(textarea, mergeWithDefaults(blockStyle))
|
|
51
|
+
} else {
|
|
52
|
+
result = blockStyle(textarea, style)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
insertText(textarea, result)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Insert or toggle link formatting
|
|
60
|
+
*/
|
|
61
|
+
export function insertLink(textarea, options = {}) {
|
|
62
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
63
|
+
|
|
64
|
+
const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
65
|
+
let style = mergeWithDefaults(FORMATS.link)
|
|
66
|
+
|
|
67
|
+
// Override with custom URL if provided
|
|
68
|
+
if (options.url) {
|
|
69
|
+
style.suffix = `](${options.url})`
|
|
70
|
+
style.replaceNext = ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Override with custom text if provided
|
|
74
|
+
if (options.text && !selectedText) {
|
|
75
|
+
// Insert the text and select it
|
|
76
|
+
const pos = textarea.selectionStart
|
|
77
|
+
textarea.value = textarea.value.slice(0, pos) + options.text + textarea.value.slice(pos)
|
|
78
|
+
textarea.selectionStart = pos
|
|
79
|
+
textarea.selectionEnd = pos + options.text.length
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = blockStyle(textarea, style)
|
|
83
|
+
insertText(textarea, result)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Toggle bullet list formatting
|
|
88
|
+
*/
|
|
89
|
+
export function toggleBulletList(textarea) {
|
|
90
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
91
|
+
const style = mergeWithDefaults(FORMATS.bulletList)
|
|
92
|
+
applyListStyle(textarea, style)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Toggle numbered list formatting
|
|
97
|
+
*/
|
|
98
|
+
export function toggleNumberedList(textarea) {
|
|
99
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
100
|
+
const style = mergeWithDefaults(FORMATS.numberedList)
|
|
101
|
+
applyListStyle(textarea, style)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Toggle quote formatting
|
|
106
|
+
*/
|
|
107
|
+
export function toggleQuote(textarea) {
|
|
108
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
109
|
+
const style = mergeWithDefaults(FORMATS.quote)
|
|
110
|
+
const result = multilineStyle(textarea, style)
|
|
111
|
+
insertText(textarea, result)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Insert header with specific level
|
|
116
|
+
*/
|
|
117
|
+
export function insertHeader(textarea, level = 1) {
|
|
118
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
119
|
+
if (level < 1 || level > 6) level = 1
|
|
120
|
+
|
|
121
|
+
const headerKey = `header${level === 1 ? '1' : level}`
|
|
122
|
+
const style = mergeWithDefaults(FORMATS[headerKey] || FORMATS.header1)
|
|
123
|
+
|
|
124
|
+
// Select current line
|
|
125
|
+
const value = textarea.value
|
|
126
|
+
const start = textarea.selectionStart
|
|
127
|
+
let lineStart = start
|
|
128
|
+
let lineEnd = start
|
|
129
|
+
|
|
130
|
+
// Find line boundaries
|
|
131
|
+
while (lineStart > 0 && value[lineStart - 1] !== '\n') {
|
|
132
|
+
lineStart--
|
|
133
|
+
}
|
|
134
|
+
while (lineEnd < value.length && value[lineEnd] !== '\n') {
|
|
135
|
+
lineEnd++
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
textarea.selectionStart = lineStart
|
|
139
|
+
textarea.selectionEnd = lineEnd
|
|
140
|
+
|
|
141
|
+
const currentLine = value.slice(lineStart, lineEnd)
|
|
142
|
+
|
|
143
|
+
// Remove any existing header formatting
|
|
144
|
+
const cleanedLine = currentLine.replace(/^#{1,6}\s*/, '')
|
|
145
|
+
|
|
146
|
+
// Apply new header
|
|
147
|
+
const newLine = style.prefix + cleanedLine
|
|
148
|
+
const result = {
|
|
149
|
+
text: newLine,
|
|
150
|
+
selectionStart: lineStart + style.prefix.length,
|
|
151
|
+
selectionEnd: lineStart + newLine.length
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
insertText(textarea, result)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get active formats at cursor position
|
|
159
|
+
*/
|
|
160
|
+
export function getActiveFormats(textarea) {
|
|
161
|
+
return getActive(textarea)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if format is active at cursor
|
|
166
|
+
*/
|
|
167
|
+
export function hasFormat(textarea, format) {
|
|
168
|
+
return has(textarea, format)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Expand selection based on options
|
|
173
|
+
*/
|
|
174
|
+
export function expandSelection(textarea, options = {}) {
|
|
175
|
+
expand(textarea, options)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Apply custom format
|
|
180
|
+
*/
|
|
181
|
+
export function applyCustomFormat(textarea, format) {
|
|
182
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
183
|
+
|
|
184
|
+
const style = mergeWithDefaults(format)
|
|
185
|
+
let result
|
|
186
|
+
|
|
187
|
+
if (style.multiline) {
|
|
188
|
+
const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
189
|
+
if (isMultipleLines(selectedText)) {
|
|
190
|
+
result = multilineStyle(textarea, style)
|
|
191
|
+
} else {
|
|
192
|
+
result = blockStyle(textarea, style)
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
result = blockStyle(textarea, style)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
insertText(textarea, result)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Preserve selection during callback
|
|
203
|
+
*/
|
|
204
|
+
export { preserveSelection }
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Configure undo method
|
|
208
|
+
*/
|
|
209
|
+
export { setUndoMethod }
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Default export with all functions
|
|
213
|
+
*/
|
|
214
|
+
export default {
|
|
215
|
+
toggleBold,
|
|
216
|
+
toggleItalic,
|
|
217
|
+
toggleCode,
|
|
218
|
+
insertLink,
|
|
219
|
+
toggleBulletList,
|
|
220
|
+
toggleNumberedList,
|
|
221
|
+
toggleQuote,
|
|
222
|
+
insertHeader,
|
|
223
|
+
getActiveFormats,
|
|
224
|
+
hasFormat,
|
|
225
|
+
expandSelection,
|
|
226
|
+
applyCustomFormat,
|
|
227
|
+
preserveSelection,
|
|
228
|
+
setUndoMethod
|
|
229
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block and multiline operations - EXACT match to GitHub's implementation
|
|
3
|
+
* Based on markdown-toolbar-element index.ts lines 581-670
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isMultipleLines,
|
|
8
|
+
expandSelectedText,
|
|
9
|
+
newlinesToSurroundSelectedText
|
|
10
|
+
} from '../core/selection-new.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Block style operation
|
|
14
|
+
* GitHub: index.ts lines 581-646
|
|
15
|
+
*/
|
|
16
|
+
export function blockStyle(textarea, arg) {
|
|
17
|
+
let newlinesToAppend
|
|
18
|
+
let newlinesToPrepend
|
|
19
|
+
|
|
20
|
+
const { prefix, suffix, blockPrefix, blockSuffix, replaceNext, prefixSpace, scanFor, surroundWithNewlines } = arg
|
|
21
|
+
const originalSelectionStart = textarea.selectionStart
|
|
22
|
+
const originalSelectionEnd = textarea.selectionEnd
|
|
23
|
+
|
|
24
|
+
let selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
25
|
+
let prefixToUse = isMultipleLines(selectedText) && blockPrefix.length > 0 ? `${blockPrefix}\n` : prefix
|
|
26
|
+
let suffixToUse = isMultipleLines(selectedText) && blockSuffix.length > 0 ? `\n${blockSuffix}` : suffix
|
|
27
|
+
|
|
28
|
+
if (prefixSpace) {
|
|
29
|
+
const beforeSelection = textarea.value[textarea.selectionStart - 1]
|
|
30
|
+
if (textarea.selectionStart !== 0 && beforeSelection != null && !beforeSelection.match(/\s/)) {
|
|
31
|
+
prefixToUse = ` ${prefixToUse}`
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
selectedText = expandSelectedText(textarea, prefixToUse, suffixToUse, arg.multiline)
|
|
35
|
+
let selectionStart = textarea.selectionStart
|
|
36
|
+
let selectionEnd = textarea.selectionEnd
|
|
37
|
+
const hasReplaceNext = replaceNext.length > 0 && suffixToUse.indexOf(replaceNext) > -1 && selectedText.length > 0
|
|
38
|
+
if (surroundWithNewlines) {
|
|
39
|
+
const ref = newlinesToSurroundSelectedText(textarea)
|
|
40
|
+
newlinesToAppend = ref.newlinesToAppend
|
|
41
|
+
newlinesToPrepend = ref.newlinesToPrepend
|
|
42
|
+
prefixToUse = newlinesToAppend + prefix
|
|
43
|
+
suffixToUse += newlinesToPrepend
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (selectedText.startsWith(prefixToUse) && selectedText.endsWith(suffixToUse)) {
|
|
47
|
+
const replacementText = selectedText.slice(prefixToUse.length, selectedText.length - suffixToUse.length)
|
|
48
|
+
if (originalSelectionStart === originalSelectionEnd) {
|
|
49
|
+
let position = originalSelectionStart - prefixToUse.length
|
|
50
|
+
position = Math.max(position, selectionStart)
|
|
51
|
+
position = Math.min(position, selectionStart + replacementText.length)
|
|
52
|
+
selectionStart = selectionEnd = position
|
|
53
|
+
} else {
|
|
54
|
+
selectionEnd = selectionStart + replacementText.length
|
|
55
|
+
}
|
|
56
|
+
return { text: replacementText, selectionStart, selectionEnd }
|
|
57
|
+
} else if (!hasReplaceNext) {
|
|
58
|
+
let replacementText = prefixToUse + selectedText + suffixToUse
|
|
59
|
+
selectionStart = originalSelectionStart + prefixToUse.length
|
|
60
|
+
selectionEnd = originalSelectionEnd + prefixToUse.length
|
|
61
|
+
const whitespaceEdges = selectedText.match(/^\s*|\s*$/g)
|
|
62
|
+
if (arg.trimFirst && whitespaceEdges) {
|
|
63
|
+
const leadingWhitespace = whitespaceEdges[0] || ''
|
|
64
|
+
const trailingWhitespace = whitespaceEdges[1] || ''
|
|
65
|
+
replacementText = leadingWhitespace + prefixToUse + selectedText.trim() + suffixToUse + trailingWhitespace
|
|
66
|
+
selectionStart += leadingWhitespace.length
|
|
67
|
+
selectionEnd -= trailingWhitespace.length
|
|
68
|
+
}
|
|
69
|
+
return { text: replacementText, selectionStart, selectionEnd }
|
|
70
|
+
} else if (scanFor.length > 0 && selectedText.match(scanFor)) {
|
|
71
|
+
suffixToUse = suffixToUse.replace(replaceNext, selectedText)
|
|
72
|
+
const replacementText = prefixToUse + selectedText + suffixToUse
|
|
73
|
+
selectionStart = selectionEnd = selectionStart + prefixToUse.length
|
|
74
|
+
return { text: replacementText, selectionStart, selectionEnd }
|
|
75
|
+
} else {
|
|
76
|
+
const replacementText = prefixToUse + selectedText + suffixToUse
|
|
77
|
+
selectionStart = selectionStart + prefixToUse.length + selectedText.length + suffixToUse.indexOf(replaceNext)
|
|
78
|
+
selectionEnd = selectionStart + replaceNext.length
|
|
79
|
+
return { text: replacementText, selectionStart, selectionEnd }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Multiline style operation
|
|
85
|
+
* GitHub: index.ts lines 648-670
|
|
86
|
+
*/
|
|
87
|
+
export function multilineStyle(textarea, arg) {
|
|
88
|
+
const { prefix, suffix, surroundWithNewlines } = arg
|
|
89
|
+
let text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
90
|
+
let selectionStart = textarea.selectionStart
|
|
91
|
+
let selectionEnd = textarea.selectionEnd
|
|
92
|
+
const lines = text.split('\n')
|
|
93
|
+
const undoStyle = lines.every(line => line.startsWith(prefix) && line.endsWith(suffix))
|
|
94
|
+
|
|
95
|
+
if (undoStyle) {
|
|
96
|
+
text = lines.map(line => line.slice(prefix.length, line.length - suffix.length)).join('\n')
|
|
97
|
+
selectionEnd = selectionStart + text.length
|
|
98
|
+
} else {
|
|
99
|
+
text = lines.map(line => prefix + line + suffix).join('\n')
|
|
100
|
+
if (surroundWithNewlines) {
|
|
101
|
+
const { newlinesToAppend, newlinesToPrepend } = newlinesToSurroundSelectedText(textarea)
|
|
102
|
+
selectionStart += newlinesToAppend.length
|
|
103
|
+
selectionEnd = selectionStart + text.length
|
|
104
|
+
text = newlinesToAppend + text + newlinesToPrepend
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { text, selectionStart, selectionEnd }
|
|
109
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block-level text formatting operations
|
|
3
|
+
* Handles inline formats like bold, italic, code, and links
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { expandSelectedText, newlinesToSurroundSelectedText, isMultipleLines } from '../core/selection.js'
|
|
7
|
+
import { insertText } from '../core/insertion.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Apply block-level styling to selected text
|
|
11
|
+
*/
|
|
12
|
+
export function blockStyle(textarea, style) {
|
|
13
|
+
let newlinesToAppend
|
|
14
|
+
let newlinesToPrepend
|
|
15
|
+
|
|
16
|
+
const { prefix, suffix, blockPrefix, blockSuffix, replaceNext, prefixSpace, scanFor, surroundWithNewlines, trimFirst } = style
|
|
17
|
+
const originalSelectionStart = textarea.selectionStart
|
|
18
|
+
const originalSelectionEnd = textarea.selectionEnd
|
|
19
|
+
|
|
20
|
+
let selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
21
|
+
let prefixToUse = isMultipleLines(selectedText) && blockPrefix && blockPrefix.length > 0 ? `${blockPrefix}\n` : prefix
|
|
22
|
+
let suffixToUse = isMultipleLines(selectedText) && blockSuffix && blockSuffix.length > 0 ? `\n${blockSuffix}` : suffix
|
|
23
|
+
|
|
24
|
+
if (prefixSpace) {
|
|
25
|
+
const beforeSelection = textarea.value[textarea.selectionStart - 1]
|
|
26
|
+
if (textarea.selectionStart !== 0 && beforeSelection != null && !beforeSelection.match(/\s/)) {
|
|
27
|
+
prefixToUse = ` ${prefixToUse}`
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
selectedText = expandSelectedText(textarea, prefixToUse, suffixToUse, style.multiline)
|
|
32
|
+
let selectionStart = textarea.selectionStart
|
|
33
|
+
let selectionEnd = textarea.selectionEnd
|
|
34
|
+
const hasReplaceNext = replaceNext && replaceNext.length > 0 && suffixToUse.indexOf(replaceNext) > -1 && selectedText.length > 0
|
|
35
|
+
|
|
36
|
+
if (surroundWithNewlines) {
|
|
37
|
+
const ref = newlinesToSurroundSelectedText(textarea)
|
|
38
|
+
newlinesToAppend = ref.newlinesToAppend
|
|
39
|
+
newlinesToPrepend = ref.newlinesToPrepend
|
|
40
|
+
prefixToUse = newlinesToAppend + prefix
|
|
41
|
+
suffixToUse += newlinesToPrepend
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if we should remove formatting (toggle off)
|
|
45
|
+
if (selectedText.startsWith(prefixToUse) && selectedText.endsWith(suffixToUse)) {
|
|
46
|
+
const replacementText = selectedText.slice(prefixToUse.length, selectedText.length - suffixToUse.length)
|
|
47
|
+
if (originalSelectionStart === originalSelectionEnd) {
|
|
48
|
+
let position = originalSelectionStart - prefixToUse.length
|
|
49
|
+
position = Math.max(position, selectionStart)
|
|
50
|
+
position = Math.min(position, selectionStart + replacementText.length)
|
|
51
|
+
selectionStart = selectionEnd = position
|
|
52
|
+
} else {
|
|
53
|
+
selectionEnd = selectionStart + replacementText.length
|
|
54
|
+
}
|
|
55
|
+
return { text: replacementText, selectionStart, selectionEnd }
|
|
56
|
+
} else if (!hasReplaceNext) {
|
|
57
|
+
// Add formatting
|
|
58
|
+
let replacementText = prefixToUse + selectedText + suffixToUse
|
|
59
|
+
selectionStart = originalSelectionStart + prefixToUse.length
|
|
60
|
+
selectionEnd = originalSelectionEnd + prefixToUse.length
|
|
61
|
+
const whitespaceEdges = selectedText.match(/^\s*|\s*$/g)
|
|
62
|
+
if (trimFirst && whitespaceEdges) {
|
|
63
|
+
const leadingWhitespace = whitespaceEdges[0] || ''
|
|
64
|
+
const trailingWhitespace = whitespaceEdges[1] || ''
|
|
65
|
+
replacementText = leadingWhitespace + prefixToUse + selectedText.trim() + suffixToUse + trailingWhitespace
|
|
66
|
+
selectionStart += leadingWhitespace.length
|
|
67
|
+
selectionEnd -= trailingWhitespace.length
|
|
68
|
+
}
|
|
69
|
+
return { text: replacementText, selectionStart, selectionEnd }
|
|
70
|
+
} else if (scanFor && scanFor.length > 0 && selectedText.match(scanFor)) {
|
|
71
|
+
// Handle link/image with URL detection
|
|
72
|
+
suffixToUse = suffixToUse.replace(replaceNext, selectedText)
|
|
73
|
+
const replacementText = prefixToUse + suffixToUse
|
|
74
|
+
selectionStart = selectionEnd = selectionStart + prefixToUse.length
|
|
75
|
+
return { text: replacementText, selectionStart, selectionEnd }
|
|
76
|
+
} else {
|
|
77
|
+
// Handle link/image with placeholder
|
|
78
|
+
const replacementText = prefixToUse + selectedText + suffixToUse
|
|
79
|
+
selectionStart = selectionStart + prefixToUse.length + selectedText.length + suffixToUse.indexOf(replaceNext)
|
|
80
|
+
selectionEnd = selectionStart + replaceNext.length
|
|
81
|
+
return { text: replacementText, selectionStart, selectionEnd }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Apply style to selected text in textarea
|
|
87
|
+
*/
|
|
88
|
+
export function styleSelectedText(textarea, style) {
|
|
89
|
+
const text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
90
|
+
|
|
91
|
+
let result
|
|
92
|
+
if (style.orderedList || style.unorderedList) {
|
|
93
|
+
// Will be handled by list operations
|
|
94
|
+
return
|
|
95
|
+
} else if (style.multiline && isMultipleLines(text)) {
|
|
96
|
+
result = multilineStyle(textarea, style)
|
|
97
|
+
} else {
|
|
98
|
+
result = blockStyle(textarea, style)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
insertText(textarea, result)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Apply multiline styling (quotes, etc)
|
|
106
|
+
*/
|
|
107
|
+
export function multilineStyle(textarea, style) {
|
|
108
|
+
const { prefix, suffix, surroundWithNewlines } = style
|
|
109
|
+
let text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
110
|
+
let selectionStart = textarea.selectionStart
|
|
111
|
+
let selectionEnd = textarea.selectionEnd
|
|
112
|
+
const lines = text.split('\n')
|
|
113
|
+
|
|
114
|
+
// Check if we need to undo (all lines have the format)
|
|
115
|
+
const undoStyle = lines.every(line => line.startsWith(prefix) && (!suffix || line.endsWith(suffix)))
|
|
116
|
+
|
|
117
|
+
if (undoStyle) {
|
|
118
|
+
// Remove the formatting
|
|
119
|
+
text = lines.map(line => {
|
|
120
|
+
let result = line.slice(prefix.length)
|
|
121
|
+
if (suffix) {
|
|
122
|
+
result = result.slice(0, result.length - suffix.length)
|
|
123
|
+
}
|
|
124
|
+
return result
|
|
125
|
+
}).join('\n')
|
|
126
|
+
selectionEnd = selectionStart + text.length
|
|
127
|
+
} else {
|
|
128
|
+
// Apply the formatting
|
|
129
|
+
text = lines.map(line => prefix + line + (suffix || '')).join('\n')
|
|
130
|
+
if (surroundWithNewlines) {
|
|
131
|
+
const { newlinesToAppend, newlinesToPrepend } = newlinesToSurroundSelectedText(textarea)
|
|
132
|
+
selectionStart += newlinesToAppend.length
|
|
133
|
+
selectionEnd = selectionStart + text.length
|
|
134
|
+
text = newlinesToAppend + text + newlinesToPrepend
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { text, selectionStart, selectionEnd }
|
|
139
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List operations - EXACT match to GitHub's implementation
|
|
3
|
+
* Based on markdown-toolbar-element index.ts lines 676-784
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { expandSelectionToLine, newlinesToSurroundSelectedText } from '../core/selection-new.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Undo ordered list style
|
|
10
|
+
* GitHub: index.ts lines 676-689
|
|
11
|
+
*/
|
|
12
|
+
function undoOrderedListStyle(text) {
|
|
13
|
+
const lines = text.split('\n')
|
|
14
|
+
const orderedListRegex = /^\d+\.\s+/
|
|
15
|
+
const shouldUndoOrderedList = lines.every(line => orderedListRegex.test(line))
|
|
16
|
+
let result = lines
|
|
17
|
+
if (shouldUndoOrderedList) {
|
|
18
|
+
result = lines.map(line => line.replace(orderedListRegex, ''))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
text: result.join('\n'),
|
|
23
|
+
processed: shouldUndoOrderedList
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Undo unordered list style
|
|
29
|
+
* GitHub: index.ts lines 691-704
|
|
30
|
+
*/
|
|
31
|
+
function undoUnorderedListStyle(text) {
|
|
32
|
+
const lines = text.split('\n')
|
|
33
|
+
const unorderedListPrefix = '- '
|
|
34
|
+
const shouldUndoUnorderedList = lines.every(line => line.startsWith(unorderedListPrefix))
|
|
35
|
+
let result = lines
|
|
36
|
+
if (shouldUndoUnorderedList) {
|
|
37
|
+
result = lines.map(line => line.slice(unorderedListPrefix.length, line.length))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
text: result.join('\n'),
|
|
42
|
+
processed: shouldUndoUnorderedList
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Make prefix for list item
|
|
48
|
+
* GitHub: index.ts lines 706-712
|
|
49
|
+
*/
|
|
50
|
+
function makePrefix(index, unorderedList) {
|
|
51
|
+
if (unorderedList) {
|
|
52
|
+
return '- '
|
|
53
|
+
} else {
|
|
54
|
+
return `${index + 1}. `
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Clear existing list style
|
|
60
|
+
* GitHub: index.ts lines 714-728
|
|
61
|
+
*/
|
|
62
|
+
function clearExistingListStyle(style, selectedText) {
|
|
63
|
+
let undoResultOpositeList
|
|
64
|
+
let undoResult
|
|
65
|
+
let pristineText
|
|
66
|
+
if (style.orderedList) {
|
|
67
|
+
undoResult = undoOrderedListStyle(selectedText)
|
|
68
|
+
undoResultOpositeList = undoUnorderedListStyle(undoResult.text)
|
|
69
|
+
pristineText = undoResultOpositeList.text
|
|
70
|
+
} else {
|
|
71
|
+
undoResult = undoUnorderedListStyle(selectedText)
|
|
72
|
+
undoResultOpositeList = undoOrderedListStyle(undoResult.text)
|
|
73
|
+
pristineText = undoResultOpositeList.text
|
|
74
|
+
}
|
|
75
|
+
return [undoResult, undoResultOpositeList, pristineText]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Apply list style to text
|
|
80
|
+
* GitHub: index.ts lines 730-784
|
|
81
|
+
* CRITICAL: This function modifies the textarea selection via expandSelectionToLine
|
|
82
|
+
*/
|
|
83
|
+
export function listStyle(textarea, style) {
|
|
84
|
+
const noInitialSelection = textarea.selectionStart === textarea.selectionEnd
|
|
85
|
+
let selectionStart = textarea.selectionStart
|
|
86
|
+
let selectionEnd = textarea.selectionEnd
|
|
87
|
+
|
|
88
|
+
// Select whole line - MODIFIES textarea.selectionStart and textarea.selectionEnd
|
|
89
|
+
expandSelectionToLine(textarea)
|
|
90
|
+
|
|
91
|
+
const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
92
|
+
|
|
93
|
+
// If the user intent was to do an undo, we will stop after this.
|
|
94
|
+
// Otherwise, we will still undo to other list type to prevent list stacking
|
|
95
|
+
const [undoResult, undoResultOpositeList, pristineText] = clearExistingListStyle(style, selectedText)
|
|
96
|
+
|
|
97
|
+
const prefixedLines = pristineText.split('\n').map((value, index) => {
|
|
98
|
+
return `${makePrefix(index, style.unorderedList)}${value}`
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const totalPrefixLength = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => {
|
|
102
|
+
return previousValue + makePrefix(currentIndex, style.unorderedList).length
|
|
103
|
+
}, 0)
|
|
104
|
+
|
|
105
|
+
const totalPrefixLengthOpositeList = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => {
|
|
106
|
+
return previousValue + makePrefix(currentIndex, !style.unorderedList).length
|
|
107
|
+
}, 0)
|
|
108
|
+
|
|
109
|
+
if (undoResult.processed) {
|
|
110
|
+
if (noInitialSelection) {
|
|
111
|
+
selectionStart = Math.max(selectionStart - makePrefix(0, style.unorderedList).length, 0)
|
|
112
|
+
selectionEnd = selectionStart
|
|
113
|
+
} else {
|
|
114
|
+
selectionStart = textarea.selectionStart
|
|
115
|
+
selectionEnd = textarea.selectionEnd - totalPrefixLength
|
|
116
|
+
}
|
|
117
|
+
return { text: pristineText, selectionStart, selectionEnd }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { newlinesToAppend, newlinesToPrepend } = newlinesToSurroundSelectedText(textarea)
|
|
121
|
+
const text = newlinesToAppend + prefixedLines.join('\n') + newlinesToPrepend
|
|
122
|
+
|
|
123
|
+
if (noInitialSelection) {
|
|
124
|
+
selectionStart = Math.max(selectionStart + makePrefix(0, style.unorderedList).length + newlinesToAppend.length, 0)
|
|
125
|
+
selectionEnd = selectionStart
|
|
126
|
+
} else {
|
|
127
|
+
if (undoResultOpositeList.processed) {
|
|
128
|
+
selectionStart = Math.max(textarea.selectionStart + newlinesToAppend.length, 0)
|
|
129
|
+
selectionEnd = textarea.selectionEnd + newlinesToAppend.length + totalPrefixLength - totalPrefixLengthOpositeList
|
|
130
|
+
} else {
|
|
131
|
+
selectionStart = Math.max(textarea.selectionStart + newlinesToAppend.length, 0)
|
|
132
|
+
selectionEnd = textarea.selectionEnd + newlinesToAppend.length + totalPrefixLength
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { text, selectionStart, selectionEnd }
|
|
137
|
+
}
|