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/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Original Work:
|
|
4
|
+
Copyright (c) 2017-2018 GitHub, Inc.
|
|
5
|
+
|
|
6
|
+
Derivative Work (markdown-actions):
|
|
7
|
+
Copyright (c) 2024
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
This software is derived from markdown-toolbar-element by GitHub, Inc.
|
|
30
|
+
https://github.com/github/markdown-toolbar-element
|
|
31
|
+
|
|
32
|
+
The core text manipulation algorithms, selection handling logic, and approach
|
|
33
|
+
to markdown formatting were extracted and adapted from GitHub's implementation.
|
package/README.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# markdown-actions
|
|
2
|
+
|
|
3
|
+
A lightweight, zero-dependency JavaScript library for adding markdown formatting functionality to any textarea element. Extracted and refined from [GitHub's markdown-toolbar-element](https://github.com/github/markdown-toolbar-element).
|
|
4
|
+
|
|
5
|
+
## ๐ฏ What is this?
|
|
6
|
+
|
|
7
|
+
`markdown-actions` provides a simple, functional API for implementing markdown toolbar buttons that work with any `<textarea>`. It handles all the complex text manipulation, selection management, and cursor positioning that makes markdown editing feel natural.
|
|
8
|
+
|
|
9
|
+
### Key Features
|
|
10
|
+
|
|
11
|
+
- ๐ชถ **Lightweight** - ~3-4kb gzipped, zero dependencies
|
|
12
|
+
- ๐จ **Framework agnostic** - Works with React, Vue, vanilla JS, or any framework
|
|
13
|
+
- โก **Simple API** - Just pass a textarea element to any function
|
|
14
|
+
- ๐ **Smart toggling** - Automatically detects and removes existing formatting
|
|
15
|
+
- ๐ **Native undo/redo** - Integrates with browser's built-in undo stack
|
|
16
|
+
- ๐ฏ **Precise cursor control** - Maintains logical cursor position after operations
|
|
17
|
+
- ๐ณ **Tree-shakeable** - Import only the functions you need
|
|
18
|
+
|
|
19
|
+
## ๐ Origin Story
|
|
20
|
+
|
|
21
|
+
This library was born from the need for a simple, reliable markdown toolbar implementation. After discovering that building a tokenizer-based approach led to complex state management and text corruption issues, we studied GitHub's battle-tested [markdown-toolbar-element](https://github.com/github/markdown-toolbar-element) and extracted its core logic.
|
|
22
|
+
|
|
23
|
+
The key insight: **you don't need to parse markdown to manipulate it effectively**. Instead of tokenizing, this library uses direct string manipulation with smart selection expansion - a simpler, more reliable approach proven in production on GitHub.com.
|
|
24
|
+
|
|
25
|
+
## ๐ Quick Start
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<!-- Your HTML -->
|
|
29
|
+
<button id="bold-btn">Bold</button>
|
|
30
|
+
<textarea id="editor"></textarea>
|
|
31
|
+
|
|
32
|
+
<!-- Using the library -->
|
|
33
|
+
<script type="module">
|
|
34
|
+
import { toggleBold } from './markdown-actions/src/index.js'
|
|
35
|
+
|
|
36
|
+
const textarea = document.getElementById('editor')
|
|
37
|
+
|
|
38
|
+
document.getElementById('bold-btn').addEventListener('click', () => {
|
|
39
|
+
toggleBold(textarea)
|
|
40
|
+
textarea.focus()
|
|
41
|
+
})
|
|
42
|
+
</script>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## ๐ API Reference
|
|
46
|
+
|
|
47
|
+
### Formatting Functions
|
|
48
|
+
|
|
49
|
+
All functions take a `HTMLTextAreaElement` as their first parameter:
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
toggleBold(textarea) // **text**
|
|
53
|
+
toggleItalic(textarea) // _text_
|
|
54
|
+
toggleCode(textarea) // `code` or ```block```
|
|
55
|
+
toggleQuote(textarea) // > quote
|
|
56
|
+
toggleBulletList(textarea) // - item
|
|
57
|
+
toggleNumberedList(textarea) // 1. item
|
|
58
|
+
insertLink(textarea, options) // [text](url)
|
|
59
|
+
insertHeader(textarea, level) // # Header
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Utility Functions
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// Get active formats at cursor position
|
|
66
|
+
getActiveFormats(textarea) // Returns: ['bold', 'italic', ...]
|
|
67
|
+
|
|
68
|
+
// Check if specific format is active
|
|
69
|
+
hasFormat(textarea, 'bold') // Returns: true/false
|
|
70
|
+
|
|
71
|
+
// Expand selection to word or line boundaries
|
|
72
|
+
expandSelection(textarea, { toWord: true })
|
|
73
|
+
|
|
74
|
+
// Apply custom markdown format
|
|
75
|
+
applyCustomFormat(textarea, {
|
|
76
|
+
prefix: '==',
|
|
77
|
+
suffix: '==',
|
|
78
|
+
trimFirst: true
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Options
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
// Link options
|
|
86
|
+
insertLink(textarea, {
|
|
87
|
+
url: 'https://example.com', // Pre-fill URL
|
|
88
|
+
text: 'Link text' // Pre-fill text
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## ๐ก Usage Examples
|
|
93
|
+
|
|
94
|
+
### React Component
|
|
95
|
+
|
|
96
|
+
```jsx
|
|
97
|
+
import { toggleBold, toggleItalic, getActiveFormats } from 'markdown-actions'
|
|
98
|
+
|
|
99
|
+
function MarkdownEditor() {
|
|
100
|
+
const textareaRef = useRef()
|
|
101
|
+
const [activeFormats, setActiveFormats] = useState([])
|
|
102
|
+
|
|
103
|
+
const updateFormats = () => {
|
|
104
|
+
setActiveFormats(getActiveFormats(textareaRef.current))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const handleBold = () => {
|
|
108
|
+
toggleBold(textareaRef.current)
|
|
109
|
+
updateFormats()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<>
|
|
114
|
+
<button
|
|
115
|
+
className={activeFormats.includes('bold') ? 'active' : ''}
|
|
116
|
+
onClick={handleBold}>
|
|
117
|
+
Bold
|
|
118
|
+
</button>
|
|
119
|
+
<textarea
|
|
120
|
+
ref={textareaRef}
|
|
121
|
+
onSelect={updateFormats}
|
|
122
|
+
onKeyUp={updateFormats}
|
|
123
|
+
/>
|
|
124
|
+
</>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Vue Component
|
|
130
|
+
|
|
131
|
+
```vue
|
|
132
|
+
<template>
|
|
133
|
+
<div>
|
|
134
|
+
<button @click="makeBold" :class="{ active: isBold }">Bold</button>
|
|
135
|
+
<textarea ref="editor" @select="checkFormats"></textarea>
|
|
136
|
+
</div>
|
|
137
|
+
</template>
|
|
138
|
+
|
|
139
|
+
<script>
|
|
140
|
+
import { toggleBold, hasFormat } from 'markdown-actions'
|
|
141
|
+
|
|
142
|
+
export default {
|
|
143
|
+
data() {
|
|
144
|
+
return {
|
|
145
|
+
isBold: false
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
methods: {
|
|
149
|
+
makeBold() {
|
|
150
|
+
toggleBold(this.$refs.editor)
|
|
151
|
+
this.checkFormats()
|
|
152
|
+
},
|
|
153
|
+
checkFormats() {
|
|
154
|
+
this.isBold = hasFormat(this.$refs.editor, 'bold')
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
</script>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Keyboard Shortcuts
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
textarea.addEventListener('keydown', (e) => {
|
|
165
|
+
if (e.ctrlKey || e.metaKey) {
|
|
166
|
+
switch(e.key) {
|
|
167
|
+
case 'b':
|
|
168
|
+
e.preventDefault()
|
|
169
|
+
toggleBold(textarea)
|
|
170
|
+
break
|
|
171
|
+
case 'i':
|
|
172
|
+
e.preventDefault()
|
|
173
|
+
toggleItalic(textarea)
|
|
174
|
+
break
|
|
175
|
+
case 'k':
|
|
176
|
+
e.preventDefault()
|
|
177
|
+
insertLink(textarea)
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Custom Formats
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
// Highlight format
|
|
188
|
+
applyCustomFormat(textarea, {
|
|
189
|
+
prefix: '==',
|
|
190
|
+
suffix: '==',
|
|
191
|
+
trimFirst: true
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// Spoiler format
|
|
195
|
+
applyCustomFormat(textarea, {
|
|
196
|
+
prefix: '||',
|
|
197
|
+
suffix: '||'
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// Custom block format
|
|
201
|
+
applyCustomFormat(textarea, {
|
|
202
|
+
prefix: '>>> ',
|
|
203
|
+
multiline: true,
|
|
204
|
+
surroundWithNewlines: true
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## ๐๏ธ How It Works
|
|
209
|
+
|
|
210
|
+
Unlike complex tokenizer-based approaches, `markdown-actions` uses a simpler, more reliable method:
|
|
211
|
+
|
|
212
|
+
1. **Smart Selection** - Expands selections to word/line boundaries when needed
|
|
213
|
+
2. **Pattern Matching** - Simple string checks to detect existing formatting
|
|
214
|
+
3. **Direct Manipulation** - Modifies text directly without parsing
|
|
215
|
+
4. **Position Tracking** - Calculates new cursor positions based on changes
|
|
216
|
+
5. **Native Integration** - Uses `document.execCommand` for undo/redo support
|
|
217
|
+
|
|
218
|
+
This approach avoids the pitfalls of tokenization:
|
|
219
|
+
- No position corruption
|
|
220
|
+
- No text duplication
|
|
221
|
+
- No state management complexity
|
|
222
|
+
- No parsing edge cases
|
|
223
|
+
|
|
224
|
+
### ๐ฏ Selection Preservation - How Text Stays Selected
|
|
225
|
+
|
|
226
|
+
One of the most polished features of `markdown-actions` is how it preserves text selection after formatting. When you click "Bold" in a toolbar, the text stays selected, allowing for immediate additional formatting. Here's the step-by-step process:
|
|
227
|
+
|
|
228
|
+
#### Step 1: Capture Original Selection
|
|
229
|
+
```javascript
|
|
230
|
+
// In blockStyle() - src/operations/block.js:17-18
|
|
231
|
+
const originalSelectionStart = textarea.selectionStart
|
|
232
|
+
const originalSelectionEnd = textarea.selectionEnd
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Step 2: Calculate New Selection After Formatting
|
|
236
|
+
When adding formatting (e.g., `**text**`):
|
|
237
|
+
```javascript
|
|
238
|
+
// src/operations/block.js:58-60
|
|
239
|
+
let replacementText = prefixToUse + selectedText + suffixToUse
|
|
240
|
+
selectionStart = originalSelectionStart + prefixToUse.length
|
|
241
|
+
selectionEnd = originalSelectionEnd + prefixToUse.length
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
When removing formatting (toggle off):
|
|
245
|
+
```javascript
|
|
246
|
+
// src/operations/block.js:46-54
|
|
247
|
+
const replacementText = selectedText.slice(prefixToUse.length, -suffixToUse.length)
|
|
248
|
+
if (originalSelectionStart === originalSelectionEnd) {
|
|
249
|
+
// Cursor position (no selection)
|
|
250
|
+
let position = originalSelectionStart - prefixToUse.length
|
|
251
|
+
selectionStart = selectionEnd = position
|
|
252
|
+
} else {
|
|
253
|
+
// Keep selection on unformatted text
|
|
254
|
+
selectionEnd = selectionStart + replacementText.length
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### Step 3: Apply Text Change with Native Undo Support
|
|
259
|
+
```javascript
|
|
260
|
+
// In insertText() - src/core/insertion.js:24-31
|
|
261
|
+
textarea.contentEditable = 'true'
|
|
262
|
+
try {
|
|
263
|
+
// Try native method first (preserves undo/redo)
|
|
264
|
+
canInsertText = document.execCommand('insertText', false, text)
|
|
265
|
+
} catch (error) {
|
|
266
|
+
canInsertText = false
|
|
267
|
+
}
|
|
268
|
+
textarea.contentEditable = 'false'
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### Step 4: Restore Selection at Calculated Position
|
|
272
|
+
```javascript
|
|
273
|
+
// src/core/insertion.js:56-60
|
|
274
|
+
if (selectionStart != null && selectionEnd != null) {
|
|
275
|
+
textarea.setSelectionRange(selectionStart, selectionEnd)
|
|
276
|
+
} else {
|
|
277
|
+
textarea.setSelectionRange(originalSelectionStart, textarea.selectionEnd)
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### Why This Matters
|
|
282
|
+
This careful selection management creates a professional editing experience:
|
|
283
|
+
- **Multiple formats**: Apply bold, then immediately italic without reselecting
|
|
284
|
+
- **Visual feedback**: See exactly what text is affected
|
|
285
|
+
- **Keyboard friendly**: Selection persists for keyboard shortcuts
|
|
286
|
+
- **Undo/redo aware**: Selection state is part of the undo stack
|
|
287
|
+
|
|
288
|
+
#### Example Flow
|
|
289
|
+
1. User selects "hello world"
|
|
290
|
+
2. User clicks Bold button
|
|
291
|
+
3. Text becomes "**hello world**"
|
|
292
|
+
4. "hello world" remains selected (without the ** markers)
|
|
293
|
+
5. User can immediately click Italic to get "**_hello world_**"
|
|
294
|
+
|
|
295
|
+
## ๐งช Testing
|
|
296
|
+
|
|
297
|
+
Open `test.html` in a browser to run the comprehensive test suite with 38 test cases covering:
|
|
298
|
+
|
|
299
|
+
- All formatting functions
|
|
300
|
+
- Edge cases and boundary conditions
|
|
301
|
+
- Selection management
|
|
302
|
+
- Format detection
|
|
303
|
+
- Custom formats
|
|
304
|
+
|
|
305
|
+
## ๐ฆ Installation
|
|
306
|
+
|
|
307
|
+
Currently, this is a standalone library. To use it:
|
|
308
|
+
|
|
309
|
+
1. Copy the `markdown-actions` folder to your project
|
|
310
|
+
2. Import the functions you need:
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
import { toggleBold, toggleItalic } from './markdown-actions/src/index.js'
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Future npm Package
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Coming soon
|
|
320
|
+
npm install markdown-actions
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## ๐ค Contributing
|
|
324
|
+
|
|
325
|
+
Contributions are welcome! The codebase is intentionally simple and well-commented. Key files:
|
|
326
|
+
|
|
327
|
+
- `src/index.js` - Public API functions
|
|
328
|
+
- `src/core/` - Core utilities (selection, insertion, formats)
|
|
329
|
+
- `src/operations/` - Text manipulation logic
|
|
330
|
+
- `demo.html` - Interactive demonstration
|
|
331
|
+
- `test.html` - Test suite
|
|
332
|
+
|
|
333
|
+
## ๐ License
|
|
334
|
+
|
|
335
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
336
|
+
|
|
337
|
+
This project is derived from [GitHub's markdown-toolbar-element](https://github.com/github/markdown-toolbar-element), which is also MIT licensed.
|
|
338
|
+
|
|
339
|
+
## ๐ Credits
|
|
340
|
+
|
|
341
|
+
This library is based on the excellent work by GitHub, Inc. on [markdown-toolbar-element](https://github.com/github/markdown-toolbar-element).
|
|
342
|
+
|
|
343
|
+
**Original Copyright:** Copyright (c) 2017-2018 GitHub, Inc.
|
|
344
|
+
|
|
345
|
+
The core text manipulation logic, selection handling algorithms, and the approach to markdown formatting were extracted and adapted from GitHub's implementation. We're grateful for their decision to open-source this battle-tested code that powers markdown editing on GitHub.com.
|
|
346
|
+
|
|
347
|
+
### Key Contributors
|
|
348
|
+
|
|
349
|
+
- **GitHub, Inc.** - Original implementation and algorithms
|
|
350
|
+
- **markdown-toolbar-element contributors** - Ongoing improvements to the source implementation
|
|
351
|
+
|
|
352
|
+
## ๐ฎ Future Plans
|
|
353
|
+
|
|
354
|
+
- [ ] npm package publication
|
|
355
|
+
- [ ] TypeScript definitions
|
|
356
|
+
- [ ] Table formatting support
|
|
357
|
+
- [ ] Footnote support
|
|
358
|
+
- [ ] More custom format templates
|
|
359
|
+
- [ ] Plugin system for extensions
|
|
360
|
+
- [ ] Framework-specific wrappers (React hooks, Vue composables)
|
|
361
|
+
|
|
362
|
+
## ๐ Known Issues
|
|
363
|
+
|
|
364
|
+
- Multi-cursor selection not supported (browser limitation)
|
|
365
|
+
- Some IME (Input Method Editor) edge cases with Asian languages
|
|
366
|
+
- Undo grouping may vary between browsers
|
|
367
|
+
|
|
368
|
+
## ๐ Browser Support
|
|
369
|
+
|
|
370
|
+
- โ
Chrome/Edge (latest)
|
|
371
|
+
- โ
Firefox (latest)
|
|
372
|
+
- โ
Safari (latest)
|
|
373
|
+
- โ
Mobile browsers (iOS Safari, Chrome Android)
|
|
374
|
+
|
|
375
|
+
Requires: `HTMLTextAreaElement.selectionStart/End` and `setSelectionRange()`
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
Built with โค๏ธ using proven patterns from GitHub's markdown-toolbar-element
|