markdown-actions 1.0.0 → 1.0.1
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/dist/markdown-actions.esm.js +421 -187
- package/dist/markdown-actions.esm.js.map +4 -4
- package/dist/markdown-actions.js +425 -191
- package/dist/markdown-actions.js.map +4 -4
- package/dist/markdown-actions.min.js +22 -19
- package/package.json +1 -1
- package/src/core/formats.js +5 -0
- package/src/core/insertion.js +48 -4
- package/src/core/selection.js +63 -0
- package/src/debug.js +34 -0
- package/src/index.js +143 -11
- package/src/operations/block.js +2 -1
package/src/index.js
CHANGED
|
@@ -5,19 +5,29 @@
|
|
|
5
5
|
|
|
6
6
|
import { FORMATS, mergeWithDefaults } from './core/formats.js'
|
|
7
7
|
import { insertText, setUndoMethod } from './core/insertion.js'
|
|
8
|
-
import { preserveSelection, isMultipleLines } from './core/selection.js'
|
|
8
|
+
import { preserveSelection, isMultipleLines, expandSelectionToLine, applyLineOperation } from './core/selection.js'
|
|
9
9
|
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
|
+
import { debugLog, debugSelection, debugResult } from './debug.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Toggle bold formatting
|
|
15
16
|
*/
|
|
16
17
|
export function toggleBold(textarea) {
|
|
17
18
|
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
19
|
+
|
|
20
|
+
debugLog('toggleBold', 'Starting');
|
|
21
|
+
debugSelection(textarea, 'Before');
|
|
22
|
+
|
|
18
23
|
const style = mergeWithDefaults(FORMATS.bold)
|
|
19
24
|
const result = blockStyle(textarea, style)
|
|
25
|
+
|
|
26
|
+
debugResult(result);
|
|
27
|
+
|
|
20
28
|
insertText(textarea, result)
|
|
29
|
+
|
|
30
|
+
debugSelection(textarea, 'After');
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
/**
|
|
@@ -103,29 +113,66 @@ export function toggleNumberedList(textarea) {
|
|
|
103
113
|
|
|
104
114
|
/**
|
|
105
115
|
* Toggle quote formatting
|
|
116
|
+
* Matches GitHub's implementation for quotes
|
|
106
117
|
*/
|
|
107
118
|
export function toggleQuote(textarea) {
|
|
108
119
|
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
120
|
+
|
|
121
|
+
debugLog('toggleQuote', 'Starting');
|
|
122
|
+
debugSelection(textarea, 'Initial');
|
|
123
|
+
|
|
109
124
|
const style = mergeWithDefaults(FORMATS.quote)
|
|
110
|
-
|
|
125
|
+
|
|
126
|
+
// Use the reusable line operation helper
|
|
127
|
+
const result = applyLineOperation(
|
|
128
|
+
textarea,
|
|
129
|
+
(ta) => multilineStyle(ta, style),
|
|
130
|
+
{ prefix: style.prefix }
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
debugResult(result);
|
|
134
|
+
insertText(textarea, result)
|
|
135
|
+
debugSelection(textarea, 'Final');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Toggle task list formatting
|
|
140
|
+
* Matches GitHub's implementation for task lists
|
|
141
|
+
*/
|
|
142
|
+
export function toggleTaskList(textarea) {
|
|
143
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
144
|
+
|
|
145
|
+
const style = mergeWithDefaults(FORMATS.taskList)
|
|
146
|
+
|
|
147
|
+
// Use the reusable line operation helper
|
|
148
|
+
const result = applyLineOperation(
|
|
149
|
+
textarea,
|
|
150
|
+
(ta) => multilineStyle(ta, style),
|
|
151
|
+
{ prefix: style.prefix }
|
|
152
|
+
)
|
|
153
|
+
|
|
111
154
|
insertText(textarea, result)
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
/**
|
|
115
|
-
* Insert header with specific level
|
|
158
|
+
* Insert or toggle header with specific level
|
|
116
159
|
*/
|
|
117
|
-
export function insertHeader(textarea, level = 1) {
|
|
160
|
+
export function insertHeader(textarea, level = 1, toggle = false) {
|
|
118
161
|
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
119
162
|
if (level < 1 || level > 6) level = 1
|
|
120
163
|
|
|
164
|
+
// Save original cursor position before any expansion
|
|
165
|
+
const originalStart = textarea.selectionStart
|
|
166
|
+
const originalEnd = textarea.selectionEnd
|
|
167
|
+
const noInitialSelection = originalStart === originalEnd
|
|
168
|
+
|
|
121
169
|
const headerKey = `header${level === 1 ? '1' : level}`
|
|
122
170
|
const style = mergeWithDefaults(FORMATS[headerKey] || FORMATS.header1)
|
|
123
171
|
|
|
124
172
|
// Select current line
|
|
125
173
|
const value = textarea.value
|
|
126
|
-
|
|
127
|
-
let
|
|
128
|
-
let lineEnd = start
|
|
174
|
+
let lineStart = originalStart
|
|
175
|
+
let lineEnd = originalStart
|
|
129
176
|
|
|
130
177
|
// Find line boundaries
|
|
131
178
|
while (lineStart > 0 && value[lineStart - 1] !== '\n') {
|
|
@@ -140,20 +187,101 @@ export function insertHeader(textarea, level = 1) {
|
|
|
140
187
|
|
|
141
188
|
const currentLine = value.slice(lineStart, lineEnd)
|
|
142
189
|
|
|
190
|
+
// Check if we're removing or changing existing header
|
|
191
|
+
const existingHeaderMatch = currentLine.match(/^(#{1,6})\s*/)
|
|
192
|
+
const existingPrefixLength = existingHeaderMatch ? existingHeaderMatch[0].length : 0
|
|
193
|
+
const existingLevel = existingHeaderMatch ? existingHeaderMatch[1].length : 0
|
|
194
|
+
|
|
143
195
|
// Remove any existing header formatting
|
|
144
196
|
const cleanedLine = currentLine.replace(/^#{1,6}\s*/, '')
|
|
145
197
|
|
|
146
|
-
|
|
147
|
-
|
|
198
|
+
let newLine
|
|
199
|
+
let isRemoving = false
|
|
200
|
+
|
|
201
|
+
// If toggle mode and current line has the same header level, remove it
|
|
202
|
+
if (toggle && existingLevel === level) {
|
|
203
|
+
newLine = cleanedLine
|
|
204
|
+
isRemoving = true
|
|
205
|
+
} else {
|
|
206
|
+
// Apply new header
|
|
207
|
+
newLine = style.prefix + cleanedLine
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let selectionStart, selectionEnd
|
|
211
|
+
|
|
212
|
+
if (noInitialSelection) {
|
|
213
|
+
// Calculate where cursor should be after the operation
|
|
214
|
+
const cursorOffsetInLine = originalStart - lineStart
|
|
215
|
+
|
|
216
|
+
if (isRemoving) {
|
|
217
|
+
// We're removing the header
|
|
218
|
+
if (cursorOffsetInLine < existingPrefixLength) {
|
|
219
|
+
// Cursor was in the header prefix, move to start of line
|
|
220
|
+
selectionStart = lineStart
|
|
221
|
+
} else {
|
|
222
|
+
// Cursor was in the text, move back by prefix length
|
|
223
|
+
selectionStart = originalStart - existingPrefixLength
|
|
224
|
+
}
|
|
225
|
+
} else if (existingPrefixLength > 0) {
|
|
226
|
+
// We're replacing an existing header
|
|
227
|
+
if (cursorOffsetInLine < existingPrefixLength) {
|
|
228
|
+
// Cursor was in the header prefix, keep it at the same position
|
|
229
|
+
selectionStart = lineStart + Math.min(cursorOffsetInLine, style.prefix.length)
|
|
230
|
+
} else {
|
|
231
|
+
// Cursor was in the text, adjust by the difference in prefix lengths
|
|
232
|
+
const prefixDiff = style.prefix.length - existingPrefixLength
|
|
233
|
+
selectionStart = originalStart + prefixDiff
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
// We're adding a new header
|
|
237
|
+
// Move cursor by the length of the new prefix
|
|
238
|
+
selectionStart = originalStart + style.prefix.length
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Ensure cursor doesn't go past end of line
|
|
242
|
+
selectionStart = Math.min(selectionStart, lineStart + newLine.length)
|
|
243
|
+
selectionEnd = selectionStart
|
|
244
|
+
} else {
|
|
245
|
+
// Keep selection if there was one
|
|
246
|
+
if (isRemoving) {
|
|
247
|
+
selectionStart = lineStart
|
|
248
|
+
selectionEnd = lineStart + newLine.length
|
|
249
|
+
} else {
|
|
250
|
+
selectionStart = lineStart + style.prefix.length
|
|
251
|
+
selectionEnd = lineStart + newLine.length
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
148
255
|
const result = {
|
|
149
256
|
text: newLine,
|
|
150
|
-
selectionStart
|
|
151
|
-
selectionEnd
|
|
257
|
+
selectionStart,
|
|
258
|
+
selectionEnd
|
|
152
259
|
}
|
|
153
260
|
|
|
154
261
|
insertText(textarea, result)
|
|
155
262
|
}
|
|
156
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Toggle H1 header
|
|
266
|
+
*/
|
|
267
|
+
export function toggleH1(textarea) {
|
|
268
|
+
insertHeader(textarea, 1, true)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Toggle H2 header
|
|
273
|
+
*/
|
|
274
|
+
export function toggleH2(textarea) {
|
|
275
|
+
insertHeader(textarea, 2, true)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Toggle H3 header
|
|
280
|
+
*/
|
|
281
|
+
export function toggleH3(textarea) {
|
|
282
|
+
insertHeader(textarea, 3, true)
|
|
283
|
+
}
|
|
284
|
+
|
|
157
285
|
/**
|
|
158
286
|
* Get active formats at cursor position
|
|
159
287
|
*/
|
|
@@ -219,7 +347,11 @@ export default {
|
|
|
219
347
|
toggleBulletList,
|
|
220
348
|
toggleNumberedList,
|
|
221
349
|
toggleQuote,
|
|
350
|
+
toggleTaskList,
|
|
222
351
|
insertHeader,
|
|
352
|
+
toggleH1,
|
|
353
|
+
toggleH2,
|
|
354
|
+
toggleH3,
|
|
223
355
|
getActiveFormats,
|
|
224
356
|
hasFormat,
|
|
225
357
|
expandSelection,
|
package/src/operations/block.js
CHANGED
|
@@ -102,7 +102,8 @@ export function styleSelectedText(textarea, style) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* Apply multiline styling (quotes, etc)
|
|
105
|
+
* Apply multiline styling (quotes, task lists, etc)
|
|
106
|
+
* Note: This does NOT expand selection to line - that should be done by the caller if needed
|
|
106
107
|
*/
|
|
107
108
|
export function multilineStyle(textarea, style) {
|
|
108
109
|
const { prefix, suffix, surroundWithNewlines } = style
|