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.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Selection utilities - EXACT match to GitHub's implementation
3
+ * Based on markdown-toolbar-element index.ts
4
+ */
5
+
6
+ /**
7
+ * Check if string contains multiple lines
8
+ * GitHub: index.ts line 419-421
9
+ */
10
+ export function isMultipleLines(string) {
11
+ return string.trim().split('\n').length > 1
12
+ }
13
+
14
+ /**
15
+ * Repeat string n times
16
+ * GitHub: index.ts line 423-425
17
+ */
18
+ function repeat(string, n) {
19
+ return Array(n + 1).join(string)
20
+ }
21
+
22
+ /**
23
+ * Find word selection start
24
+ * GitHub: index.ts line 427-433
25
+ */
26
+ export function wordSelectionStart(text, i) {
27
+ let index = i
28
+ while (text[index] && text[index - 1] != null && !text[index - 1].match(/\s/)) {
29
+ index--
30
+ }
31
+ return index
32
+ }
33
+
34
+ /**
35
+ * Find word selection end
36
+ * GitHub: index.ts line 435-442
37
+ */
38
+ export function wordSelectionEnd(text, i, multiline) {
39
+ let index = i
40
+ const breakpoint = multiline ? /\n/ : /\s/
41
+ while (text[index] && !text[index].match(breakpoint)) {
42
+ index++
43
+ }
44
+ return index
45
+ }
46
+
47
+ /**
48
+ * Expand selection to line boundaries
49
+ * GitHub: index.ts line 502-515
50
+ * CRITICAL: This modifies the textarea selection directly!
51
+ */
52
+ export function expandSelectionToLine(textarea) {
53
+ const lines = textarea.value.split('\n')
54
+ let counter = 0
55
+ for (let index = 0; index < lines.length; index++) {
56
+ const lineLength = lines[index].length + 1
57
+ if (textarea.selectionStart >= counter && textarea.selectionStart < counter + lineLength) {
58
+ textarea.selectionStart = counter
59
+ }
60
+ if (textarea.selectionEnd >= counter && textarea.selectionEnd < counter + lineLength) {
61
+ textarea.selectionEnd = counter + lineLength - 1
62
+ }
63
+ counter += lineLength
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Expand selected text
69
+ * GitHub: index.ts line 517-537
70
+ * CRITICAL: This modifies the textarea selection to include format markers if they exist
71
+ */
72
+ export function expandSelectedText(textarea, prefixToUse, suffixToUse, multiline = false) {
73
+ if (textarea.selectionStart === textarea.selectionEnd) {
74
+ textarea.selectionStart = wordSelectionStart(textarea.value, textarea.selectionStart)
75
+ textarea.selectionEnd = wordSelectionEnd(textarea.value, textarea.selectionEnd, multiline)
76
+ } else {
77
+ const expandedSelectionStart = textarea.selectionStart - prefixToUse.length
78
+ const expandedSelectionEnd = textarea.selectionEnd + suffixToUse.length
79
+ const beginsWithPrefix = textarea.value.slice(expandedSelectionStart, textarea.selectionStart) === prefixToUse
80
+ const endsWithSuffix = textarea.value.slice(textarea.selectionEnd, expandedSelectionEnd) === suffixToUse
81
+ if (beginsWithPrefix && endsWithSuffix) {
82
+ textarea.selectionStart = expandedSelectionStart
83
+ textarea.selectionEnd = expandedSelectionEnd
84
+ }
85
+ }
86
+ return textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
87
+ }
88
+
89
+ /**
90
+ * Calculate newlines to surround selected text
91
+ * GitHub: index.ts line 544-573
92
+ */
93
+ export function newlinesToSurroundSelectedText(textarea) {
94
+ const beforeSelection = textarea.value.slice(0, textarea.selectionStart)
95
+ const afterSelection = textarea.value.slice(textarea.selectionEnd)
96
+
97
+ const breaksBefore = beforeSelection.match(/\n*$/)
98
+ const breaksAfter = afterSelection.match(/^\n*/)
99
+ const newlinesBeforeSelection = breaksBefore ? breaksBefore[0].length : 0
100
+ const newlinesAfterSelection = breaksAfter ? breaksAfter[0].length : 0
101
+
102
+ let newlinesToAppend
103
+ let newlinesToPrepend
104
+
105
+ if (beforeSelection.match(/\S/) && newlinesBeforeSelection < 2) {
106
+ newlinesToAppend = repeat('\n', 2 - newlinesBeforeSelection)
107
+ }
108
+
109
+ if (afterSelection.match(/\S/) && newlinesAfterSelection < 2) {
110
+ newlinesToPrepend = repeat('\n', 2 - newlinesAfterSelection)
111
+ }
112
+
113
+ if (newlinesToAppend == null) {
114
+ newlinesToAppend = ''
115
+ }
116
+
117
+ if (newlinesToPrepend == null) {
118
+ newlinesToPrepend = ''
119
+ }
120
+
121
+ return { newlinesToAppend, newlinesToPrepend }
122
+ }
123
+
124
+ /**
125
+ * Preserve selection helper (not in GitHub's but useful)
126
+ */
127
+ export function preserveSelection(textarea, callback) {
128
+ const start = textarea.selectionStart
129
+ const end = textarea.selectionEnd
130
+ const scrollTop = textarea.scrollTop
131
+
132
+ callback()
133
+
134
+ textarea.selectionStart = start
135
+ textarea.selectionEnd = end
136
+ textarea.scrollTop = scrollTop
137
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Core selection utilities extracted and adapted from GitHub's markdown-toolbar-element
3
+ */
4
+
5
+ /**
6
+ * Check if string contains multiple lines
7
+ */
8
+ export function isMultipleLines(string) {
9
+ return string.trim().split('\n').length > 1
10
+ }
11
+
12
+ /**
13
+ * Find the start of the word at position i
14
+ */
15
+ export function wordSelectionStart(text, i) {
16
+ let index = i
17
+ while (text[index] && text[index - 1] != null && !text[index - 1].match(/\s/)) {
18
+ index--
19
+ }
20
+ return index
21
+ }
22
+
23
+ /**
24
+ * Find the end of the word at position i
25
+ */
26
+ export function wordSelectionEnd(text, i, multiline) {
27
+ let index = i
28
+ const breakpoint = multiline ? /\n/ : /\s/
29
+ while (text[index] && !text[index].match(breakpoint)) {
30
+ index++
31
+ }
32
+ return index
33
+ }
34
+
35
+ /**
36
+ * Expand selection to line boundaries
37
+ */
38
+ export function expandSelectionToLine(textarea) {
39
+ const lines = textarea.value.split('\n')
40
+ let counter = 0
41
+ for (let index = 0; index < lines.length; index++) {
42
+ const lineLength = lines[index].length + 1
43
+ if (textarea.selectionStart >= counter && textarea.selectionStart < counter + lineLength) {
44
+ textarea.selectionStart = counter
45
+ }
46
+ if (textarea.selectionEnd >= counter && textarea.selectionEnd < counter + lineLength) {
47
+ // For the last line, don't go past the actual text length
48
+ if (index === lines.length - 1) {
49
+ textarea.selectionEnd = Math.min(counter + lines[index].length, textarea.value.length)
50
+ } else {
51
+ textarea.selectionEnd = counter + lineLength - 1
52
+ }
53
+ }
54
+ counter += lineLength
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Expand selected text with smart boundary detection
60
+ */
61
+ export function expandSelectedText(textarea, prefixToUse, suffixToUse, multiline = false) {
62
+ if (textarea.selectionStart === textarea.selectionEnd) {
63
+ textarea.selectionStart = wordSelectionStart(textarea.value, textarea.selectionStart)
64
+ textarea.selectionEnd = wordSelectionEnd(textarea.value, textarea.selectionEnd, multiline)
65
+ } else {
66
+ const expandedSelectionStart = textarea.selectionStart - prefixToUse.length
67
+ const expandedSelectionEnd = textarea.selectionEnd + suffixToUse.length
68
+ const beginsWithPrefix = textarea.value.slice(expandedSelectionStart, textarea.selectionStart) === prefixToUse
69
+ const endsWithSuffix = textarea.value.slice(textarea.selectionEnd, expandedSelectionEnd) === suffixToUse
70
+ if (beginsWithPrefix && endsWithSuffix) {
71
+ textarea.selectionStart = expandedSelectionStart
72
+ textarea.selectionEnd = expandedSelectionEnd
73
+ }
74
+ }
75
+ return textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
76
+ }
77
+
78
+ /**
79
+ * Calculate newlines needed to surround selected text
80
+ */
81
+ export function newlinesToSurroundSelectedText(textarea) {
82
+ const beforeSelection = textarea.value.slice(0, textarea.selectionStart)
83
+ const afterSelection = textarea.value.slice(textarea.selectionEnd)
84
+
85
+ const breaksBefore = beforeSelection.match(/\n*$/)
86
+ const breaksAfter = afterSelection.match(/^\n*/)
87
+ const newlinesBeforeSelection = breaksBefore ? breaksBefore[0].length : 0
88
+ const newlinesAfterSelection = breaksAfter ? breaksAfter[0].length : 0
89
+
90
+ let newlinesToAppend = ''
91
+ let newlinesToPrepend = ''
92
+
93
+ if (beforeSelection.match(/\S/) && newlinesBeforeSelection < 2) {
94
+ newlinesToAppend = '\n'.repeat(2 - newlinesBeforeSelection)
95
+ }
96
+
97
+ if (afterSelection.match(/\S/) && newlinesAfterSelection < 2) {
98
+ newlinesToPrepend = '\n'.repeat(2 - newlinesAfterSelection)
99
+ }
100
+
101
+ return { newlinesToAppend, newlinesToPrepend }
102
+ }
103
+
104
+ /**
105
+ * Utility to preserve selection during operations
106
+ */
107
+ export function preserveSelection(textarea, callback) {
108
+ const start = textarea.selectionStart
109
+ const end = textarea.selectionEnd
110
+ const scrollTop = textarea.scrollTop
111
+
112
+ callback()
113
+
114
+ textarea.selectionStart = start
115
+ textarea.selectionEnd = end
116
+ textarea.scrollTop = scrollTop
117
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Style arguments type definition and defaults
3
+ * Matches GitHub's implementation exactly
4
+ */
5
+
6
+ /**
7
+ * Get default style arguments
8
+ * MUST match GitHub's defaults exactly (index.ts lines 805-818)
9
+ */
10
+ export function getDefaultStyleArgs() {
11
+ return {
12
+ prefix: '',
13
+ suffix: '',
14
+ blockPrefix: '',
15
+ blockSuffix: '',
16
+ multiline: false,
17
+ replaceNext: '',
18
+ prefixSpace: false,
19
+ scanFor: '',
20
+ surroundWithNewlines: false,
21
+ orderedList: false,
22
+ unorderedList: false,
23
+ trimFirst: false
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Merge style with defaults
29
+ * Ensures ALL properties are present
30
+ */
31
+ export function mergeStyleWithDefaults(style) {
32
+ return { ...getDefaultStyleArgs(), ...style }
33
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Main text styling orchestration
3
+ * EXACT match to GitHub's implementation
4
+ * Based on markdown-toolbar-element index.ts lines 487-500
5
+ */
6
+
7
+ import { isMultipleLines } from './selection-new.js'
8
+ import { insertText } from './insertion-new.js'
9
+ import { blockStyle, multilineStyle } from '../operations/block-new.js'
10
+ import { listStyle } from '../operations/list-new.js'
11
+
12
+ /**
13
+ * Style selected text in textarea
14
+ * GitHub: index.ts lines 487-500
15
+ * CRITICAL: This is the main orchestrator that decides which operation to use
16
+ */
17
+ export function styleSelectedText(textarea, styleArgs) {
18
+ const text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
19
+
20
+ let result
21
+ if (styleArgs.orderedList || styleArgs.unorderedList) {
22
+ result = listStyle(textarea, styleArgs)
23
+ } else if (styleArgs.multiline && isMultipleLines(text)) {
24
+ result = multilineStyle(textarea, styleArgs)
25
+ } else {
26
+ result = blockStyle(textarea, styleArgs)
27
+ }
28
+
29
+ insertText(textarea, result)
30
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * markdown-actions - Lightweight markdown toolbar functionality
3
+ * Exact implementation matching GitHub's markdown-toolbar-element
4
+ */
5
+
6
+ import { mergeStyleWithDefaults } from './core/style-args.js'
7
+ import { styleSelectedText } from './core/style-text.js'
8
+ import { setUndoMethod } from './core/insertion-new.js'
9
+ import { preserveSelection, expandSelectionToLine } from './core/selection-new.js'
10
+ import { getActiveFormats as getActive, hasFormat as has } from './core/detection.js'
11
+
12
+ /**
13
+ * Format configurations matching GitHub's manualStyles
14
+ * GitHub: index.ts lines 86-118
15
+ */
16
+ const FORMATS = {
17
+ bold: { prefix: '**', suffix: '**', trimFirst: true },
18
+ italic: { prefix: '_', suffix: '_', trimFirst: true },
19
+ code: {
20
+ prefix: '`',
21
+ suffix: '`',
22
+ blockPrefix: '```',
23
+ blockSuffix: '```'
24
+ },
25
+ link: {
26
+ prefix: '[',
27
+ suffix: '](url)',
28
+ replaceNext: 'url',
29
+ scanFor: 'https?://'
30
+ },
31
+ quote: {
32
+ prefix: '> ',
33
+ multiline: true,
34
+ surroundWithNewlines: true
35
+ },
36
+ bulletList: {
37
+ prefix: '- ',
38
+ multiline: true,
39
+ unorderedList: true
40
+ },
41
+ numberedList: {
42
+ prefix: '1. ',
43
+ multiline: true,
44
+ orderedList: true
45
+ },
46
+ header1: { prefix: '# ' },
47
+ header2: { prefix: '## ' },
48
+ header3: { prefix: '### ' },
49
+ header4: { prefix: '#### ' },
50
+ header5: { prefix: '##### ' },
51
+ header6: { prefix: '###### ' }
52
+ }
53
+
54
+ /**
55
+ * Apply style to textarea
56
+ * Mimics GitHub's applyStyle function (index.ts lines 801-827)
57
+ */
58
+ function applyStyle(textarea, format) {
59
+ const style = mergeStyleWithDefaults(format)
60
+ textarea.focus()
61
+ styleSelectedText(textarea, style)
62
+ }
63
+
64
+ /**
65
+ * Toggle bold formatting
66
+ */
67
+ export function toggleBold(textarea) {
68
+ if (!textarea || textarea.disabled || textarea.readOnly) return
69
+ applyStyle(textarea, FORMATS.bold)
70
+ }
71
+
72
+ /**
73
+ * Toggle italic formatting
74
+ */
75
+ export function toggleItalic(textarea) {
76
+ if (!textarea || textarea.disabled || textarea.readOnly) return
77
+ applyStyle(textarea, FORMATS.italic)
78
+ }
79
+
80
+ /**
81
+ * Toggle code formatting
82
+ */
83
+ export function toggleCode(textarea) {
84
+ if (!textarea || textarea.disabled || textarea.readOnly) return
85
+ applyStyle(textarea, FORMATS.code)
86
+ }
87
+
88
+ /**
89
+ * Insert or toggle link formatting
90
+ */
91
+ export function insertLink(textarea, options = {}) {
92
+ if (!textarea || textarea.disabled || textarea.readOnly) return
93
+
94
+ let format = { ...FORMATS.link }
95
+
96
+ // Override with custom URL if provided
97
+ if (options.url) {
98
+ format.suffix = `](${options.url})`
99
+ format.replaceNext = ''
100
+ }
101
+
102
+ // Handle custom text if provided
103
+ if (options.text && !textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)) {
104
+ const pos = textarea.selectionStart
105
+ textarea.value = textarea.value.slice(0, pos) + options.text + textarea.value.slice(pos)
106
+ textarea.selectionStart = pos
107
+ textarea.selectionEnd = pos + options.text.length
108
+ }
109
+
110
+ applyStyle(textarea, format)
111
+ }
112
+
113
+ /**
114
+ * Toggle bullet list formatting
115
+ */
116
+ export function toggleBulletList(textarea) {
117
+ if (!textarea || textarea.disabled || textarea.readOnly) return
118
+ applyStyle(textarea, FORMATS.bulletList)
119
+ }
120
+
121
+ /**
122
+ * Toggle numbered list formatting
123
+ */
124
+ export function toggleNumberedList(textarea) {
125
+ if (!textarea || textarea.disabled || textarea.readOnly) return
126
+ applyStyle(textarea, FORMATS.numberedList)
127
+ }
128
+
129
+ /**
130
+ * Toggle quote formatting
131
+ */
132
+ export function toggleQuote(textarea) {
133
+ if (!textarea || textarea.disabled || textarea.readOnly) return
134
+ applyStyle(textarea, FORMATS.quote)
135
+ }
136
+
137
+ /**
138
+ * Insert header with specific level
139
+ */
140
+ export function insertHeader(textarea, level = 1) {
141
+ if (!textarea || textarea.disabled || textarea.readOnly) return
142
+ if (level < 1 || level > 6) level = 1
143
+
144
+ const headerKey = `header${level}`
145
+ const format = FORMATS[headerKey] || FORMATS.header1
146
+
147
+ // Select current line
148
+ const value = textarea.value
149
+ const start = textarea.selectionStart
150
+ let lineStart = start
151
+ let lineEnd = start
152
+
153
+ // Find line boundaries
154
+ while (lineStart > 0 && value[lineStart - 1] !== '\n') {
155
+ lineStart--
156
+ }
157
+ while (lineEnd < value.length && value[lineEnd] !== '\n') {
158
+ lineEnd++
159
+ }
160
+
161
+ textarea.selectionStart = lineStart
162
+ textarea.selectionEnd = lineEnd
163
+
164
+ const currentLine = value.slice(lineStart, lineEnd)
165
+
166
+ // Remove any existing header formatting
167
+ const cleanedLine = currentLine.replace(/^#{1,6}\s*/, '')
168
+
169
+ // Apply new header
170
+ const newLine = format.prefix + cleanedLine
171
+
172
+ // Use our style system
173
+ const tempSelection = textarea.selectionStart
174
+ textarea.value = value.slice(0, lineStart) + newLine + value.slice(lineEnd)
175
+ textarea.selectionStart = lineStart + format.prefix.length
176
+ textarea.selectionEnd = lineStart + newLine.length
177
+ }
178
+
179
+ /**
180
+ * Get active formats at cursor position
181
+ */
182
+ export function getActiveFormats(textarea) {
183
+ return getActive(textarea)
184
+ }
185
+
186
+ /**
187
+ * Check if format is active at cursor
188
+ */
189
+ export function hasFormat(textarea, format) {
190
+ return has(textarea, format)
191
+ }
192
+
193
+ /**
194
+ * Expand selection based on options
195
+ */
196
+ export function expandSelection(textarea, options = {}) {
197
+ if (!textarea) return
198
+
199
+ const { toWord, toLine } = options
200
+ const { selectionStart, selectionEnd, value } = textarea
201
+
202
+ if (toLine) {
203
+ expandSelectionToLine(textarea)
204
+ } else if (toWord && selectionStart === selectionEnd) {
205
+ // Find word boundaries
206
+ let start = selectionStart
207
+ let end = selectionEnd
208
+
209
+ // Move start back to word boundary
210
+ while (start > 0 && !/\s/.test(value[start - 1])) {
211
+ start--
212
+ }
213
+
214
+ // Move end forward to word boundary
215
+ while (end < value.length && !/\s/.test(value[end])) {
216
+ end++
217
+ }
218
+
219
+ textarea.selectionStart = start
220
+ textarea.selectionEnd = end
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Apply custom format
226
+ */
227
+ export function applyCustomFormat(textarea, format) {
228
+ if (!textarea || textarea.disabled || textarea.readOnly) return
229
+ applyStyle(textarea, format)
230
+ }
231
+
232
+ /**
233
+ * Preserve selection during callback
234
+ */
235
+ export { preserveSelection }
236
+
237
+ /**
238
+ * Configure undo method
239
+ */
240
+ export { setUndoMethod }
241
+
242
+ /**
243
+ * Default export with all functions
244
+ */
245
+ export default {
246
+ toggleBold,
247
+ toggleItalic,
248
+ toggleCode,
249
+ insertLink,
250
+ toggleBulletList,
251
+ toggleNumberedList,
252
+ toggleQuote,
253
+ insertHeader,
254
+ getActiveFormats,
255
+ hasFormat,
256
+ expandSelection,
257
+ applyCustomFormat,
258
+ preserveSelection,
259
+ setUndoMethod
260
+ }