cm-md-editor 2.0.0 → 2.1.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/index.html +49 -13
- package/package.json +1 -1
- package/src/MdEditor.js +159 -21
package/index.html
CHANGED
|
@@ -21,23 +21,59 @@
|
|
|
21
21
|
<div class="container-fluid">
|
|
22
22
|
<label for="editor">cm-md-editor</label>
|
|
23
23
|
<textarea id="editor">
|
|
24
|
-
#
|
|
24
|
+
# Heading 1
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
## Heading 2
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
### Heading 3
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
- Supports outlines with tab and shift-tab to indent and outdent
|
|
32
|
-
- Supports the **bold** syntax with command-b/ctrl-b
|
|
33
|
-
- Supports the _italic_ syntax with command-i/ctrl-i
|
|
34
|
-
- Supports undo and redo with command-z/ctrl-z and command-shift-z/ctrl-shift-z
|
|
35
|
-
- It is…
|
|
36
|
-
- lightweight
|
|
37
|
-
- easy to use
|
|
38
|
-
- fast
|
|
30
|
+
This is a minimal **markdown editor** with _italic_ and **_bold italic_** text. You can also use ~~strikethrough~~ for deleted text.
|
|
39
31
|
|
|
40
|
-
|
|
32
|
+
Inline `code spans` are highlighted and protect their content: `**not bold**`.
|
|
33
|
+
|
|
34
|
+
> Blockquotes are highlighted with a green prefix.
|
|
35
|
+
|
|
36
|
+
### Links and images
|
|
37
|
+
|
|
38
|
+
Visit [the demo page](https://shaack.com/projekte/cm-md-editor/) or use a [reference link][ref].
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
[ref]: https://shaack.com
|
|
43
|
+
|
|
44
|
+
### Lists
|
|
45
|
+
|
|
46
|
+
- Unordered list item
|
|
47
|
+
- Another item
|
|
48
|
+
- Nested item
|
|
49
|
+
- Deeper nesting
|
|
50
|
+
|
|
51
|
+
1. Ordered list item
|
|
52
|
+
2. Second item
|
|
53
|
+
|
|
54
|
+
### Task lists
|
|
55
|
+
|
|
56
|
+
- [ ] Unchecked task
|
|
57
|
+
- [x] Completed task
|
|
58
|
+
|
|
59
|
+
### Code block
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
const editor = new MdEditor(element)
|
|
63
|
+
console.log("syntax highlighted")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Horizontal rules
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### Escape sequences
|
|
71
|
+
|
|
72
|
+
Literal stars: \*not bold\* and backslash: \\
|
|
73
|
+
|
|
74
|
+
### HTML tags
|
|
75
|
+
|
|
76
|
+
This has <mark>inline HTML</mark> and a <br> tag.</textarea>
|
|
41
77
|
</div>
|
|
42
78
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
|
43
79
|
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
package/package.json
CHANGED
package/src/MdEditor.js
CHANGED
|
@@ -45,6 +45,28 @@ export class MdEditor {
|
|
|
45
45
|
})
|
|
46
46
|
toolbar.appendChild(button)
|
|
47
47
|
})
|
|
48
|
+
|
|
49
|
+
// Spacer to push wrap toggle to the right
|
|
50
|
+
const spacer = document.createElement('div')
|
|
51
|
+
spacer.style.cssText = 'flex:1;'
|
|
52
|
+
toolbar.appendChild(spacer)
|
|
53
|
+
|
|
54
|
+
// Wrap toggle button
|
|
55
|
+
this.wrapEnabled = true
|
|
56
|
+
this.wrapButton = document.createElement('button')
|
|
57
|
+
this.wrapButton.type = 'button'
|
|
58
|
+
this.wrapButton.title = 'Toggle word wrap'
|
|
59
|
+
this.wrapButton.style.cssText = 'background:none;border:none;border-radius:3px;cursor:pointer;padding:4px 6px;display:flex;align-items:center;justify-content:center;color:inherit;opacity:0.6;transition:opacity 0.15s,background 0.15s;'
|
|
60
|
+
this.wrapButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4zm0 10h6v2H4zm0-5h16c0 0 0 0 0 0v0c0 2.2-1.8 4-4 4h-2l2-2h-1l-3 3 3 3h1l-2-2h2c3.3 0 6-2.7 6-6H4z"/></svg>`
|
|
61
|
+
this.wrapButton.addEventListener('mouseenter', () => { this.wrapButton.style.opacity = '1'; this.wrapButton.style.background = 'rgba(128,128,128,0.2)' })
|
|
62
|
+
this.wrapButton.addEventListener('mouseleave', () => { this.wrapButton.style.opacity = this.wrapEnabled ? '0.9' : '0.4'; this.wrapButton.style.background = 'none' })
|
|
63
|
+
this.wrapButton.addEventListener('mousedown', (e) => e.preventDefault())
|
|
64
|
+
this.wrapButton.addEventListener('click', (e) => {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
this.toggleWrapMode()
|
|
67
|
+
})
|
|
68
|
+
this.wrapButton.style.opacity = '0.9'
|
|
69
|
+
toolbar.appendChild(this.wrapButton)
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
createHighlightBackdrop() {
|
|
@@ -78,13 +100,15 @@ export class MdEditor {
|
|
|
78
100
|
syncStyles()
|
|
79
101
|
|
|
80
102
|
// Make textarea background transparent so backdrop shows through
|
|
103
|
+
this.element.style.overscrollBehavior = 'none'
|
|
81
104
|
this.element.style.background = 'transparent'
|
|
82
105
|
this.element.style.position = 'relative'
|
|
83
106
|
this.element.style.caretColor = cs.color
|
|
84
107
|
|
|
85
108
|
// Sync scroll
|
|
86
109
|
this.element.addEventListener('scroll', () => {
|
|
87
|
-
this.
|
|
110
|
+
this.backdrop.scrollTop = this.element.scrollTop
|
|
111
|
+
this.backdrop.scrollLeft = this.element.scrollLeft
|
|
88
112
|
})
|
|
89
113
|
|
|
90
114
|
// Update on input
|
|
@@ -97,38 +121,152 @@ export class MdEditor {
|
|
|
97
121
|
const text = this.element.value
|
|
98
122
|
const lines = text.split('\n')
|
|
99
123
|
let html = ''
|
|
124
|
+
let inCodeBlock = false
|
|
125
|
+
|
|
100
126
|
for (let i = 0; i < lines.length; i++) {
|
|
101
127
|
if (i > 0) html += '\n'
|
|
102
|
-
|
|
103
|
-
let result = this.escapeHtml(line)
|
|
128
|
+
const line = lines[i]
|
|
104
129
|
|
|
105
|
-
//
|
|
130
|
+
// Fenced code block delimiter
|
|
131
|
+
if (/^`{3,}/.test(line)) {
|
|
132
|
+
inCodeBlock = !inCodeBlock
|
|
133
|
+
html += '<span style="color:rgba(130,170,200,0.7)">' + this.escapeHtml(line) + '</span>'
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (inCodeBlock) {
|
|
137
|
+
html += '<span style="color:rgba(130,170,200,0.5)">' + this.escapeHtml(line) + '</span>'
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Horizontal rule (3+ of same -, *, or _ with optional spaces)
|
|
142
|
+
if (/^\s{0,3}([-*_])\s*(\1\s*){2,}$/.test(line)) {
|
|
143
|
+
html += '<span style="color:rgba(128,128,128,0.6)">' + this.escapeHtml(line) + '</span>'
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Headings
|
|
106
148
|
const headingMatch = line.match(/^(#{1,6}) /)
|
|
107
149
|
if (headingMatch) {
|
|
108
|
-
const hashes = this.escapeHtml(headingMatch[1])
|
|
109
|
-
const rest = this.escapeHtml(line.substring(headingMatch[0].length))
|
|
110
150
|
const opacity = Math.max(0.3, 0.8 - (headingMatch[1].length - 1) * 0.1)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Bold **text**
|
|
114
|
-
result = result.replace(/(\*\*)(.*?)(\*\*)/g,
|
|
115
|
-
'<span style="color:rgba(255,180,80,0.5)">$1</span><span style="color:rgba(255,180,80,0.8)">$2</span><span style="color:rgba(255,180,80,0.5)">$3</span>')
|
|
116
|
-
// Italic _text_
|
|
117
|
-
result = result.replace(/((?:^|[^\\]))(\_)(.*?[^\\])(\_)/g,
|
|
118
|
-
'$1<span style="color:rgba(180,130,255,0.5)">$2</span><span style="color:rgba(180,130,255,0.8)">$3</span><span style="color:rgba(180,130,255,0.5)">$4</span>')
|
|
119
|
-
// Unordered list markers
|
|
120
|
-
result = result.replace(/^(\t*)(- )/, (_, tabs, marker) =>
|
|
121
|
-
this.escapeHtml(tabs) + '<span style="color:rgba(100,200,150,0.7)">' + this.escapeHtml(marker) + '</span>')
|
|
122
|
-
// Ordered list markers
|
|
123
|
-
result = result.replace(/^(\t*)(\d+\. )/, (_, tabs, marker) =>
|
|
124
|
-
this.escapeHtml(tabs) + '<span style="color:rgba(100,200,150,0.7)">' + this.escapeHtml(marker) + '</span>')
|
|
151
|
+
html += '<span style="color:rgba(100,160,255,' + opacity + ')">' + this.escapeHtml(line) + '</span>'
|
|
152
|
+
continue
|
|
125
153
|
}
|
|
126
|
-
|
|
154
|
+
|
|
155
|
+
// Reference link definition [ref]: url
|
|
156
|
+
const refMatch = line.match(/^(\s{0,3}\[)([^\]]+)(\]:\s+)(.+)$/)
|
|
157
|
+
if (refMatch) {
|
|
158
|
+
html += '<span style="color:rgba(100,180,220,0.5)">' + this.escapeHtml(refMatch[1]) + '</span>'
|
|
159
|
+
+ '<span style="color:rgba(100,180,220,0.8)">' + this.escapeHtml(refMatch[2]) + '</span>'
|
|
160
|
+
+ '<span style="color:rgba(100,180,220,0.5)">' + this.escapeHtml(refMatch[3]) + '</span>'
|
|
161
|
+
+ '<span style="color:rgba(100,180,220,0.6)">' + this.escapeHtml(refMatch[4]) + '</span>'
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Blockquote prefix
|
|
166
|
+
let prefix = ''
|
|
167
|
+
let rest = line
|
|
168
|
+
const bqMatch = line.match(/^(\s*>+\s?)/)
|
|
169
|
+
if (bqMatch) {
|
|
170
|
+
prefix = '<span style="color:rgba(128,180,128,0.7)">' + this.escapeHtml(bqMatch[0]) + '</span>'
|
|
171
|
+
rest = line.substring(bqMatch[0].length)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
html += prefix + this.highlightInline(rest)
|
|
127
175
|
}
|
|
128
176
|
// Trailing newline so the backdrop height matches the textarea
|
|
129
177
|
this.highlightLayer.innerHTML = html + '\n'
|
|
130
178
|
}
|
|
131
179
|
|
|
180
|
+
highlightInline(line) {
|
|
181
|
+
// Split by inline code to protect code content from other highlighting
|
|
182
|
+
const segments = []
|
|
183
|
+
let lastIndex = 0
|
|
184
|
+
const codeRegex = /`([^`]+)`/g
|
|
185
|
+
let match
|
|
186
|
+
|
|
187
|
+
while ((match = codeRegex.exec(line)) !== null) {
|
|
188
|
+
if (match.index > lastIndex) {
|
|
189
|
+
segments.push({type: 'text', content: line.substring(lastIndex, match.index)})
|
|
190
|
+
}
|
|
191
|
+
segments.push({type: 'code', content: match[0]})
|
|
192
|
+
lastIndex = codeRegex.lastIndex
|
|
193
|
+
}
|
|
194
|
+
if (lastIndex < line.length) {
|
|
195
|
+
segments.push({type: 'text', content: line.substring(lastIndex)})
|
|
196
|
+
}
|
|
197
|
+
if (segments.length === 0) {
|
|
198
|
+
segments.push({type: 'text', content: ''})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let result = ''
|
|
202
|
+
for (const seg of segments) {
|
|
203
|
+
if (seg.type === 'code') {
|
|
204
|
+
result += '<span style="color:rgba(130,170,200,0.7)">' + this.escapeHtml(seg.content) + '</span>'
|
|
205
|
+
} else {
|
|
206
|
+
result += this.highlightTextSegment(this.escapeHtml(seg.content))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return result
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
highlightTextSegment(escaped) {
|
|
213
|
+
let result = escaped
|
|
214
|
+
|
|
215
|
+
// Escape sequences: dim the backslash before markdown punctuation
|
|
216
|
+
result = result.replace(/\\([\\`*_{}[\]()#+\-.!~|])/g,
|
|
217
|
+
'<span style="color:rgba(128,128,128,0.4)">\\</span>$1')
|
|
218
|
+
|
|
219
|
+
// Unordered list markers with optional task list checkbox
|
|
220
|
+
result = result.replace(/^(\t*)(- )(\[[ xX]\] )?/, (_, tabs, marker, task) => {
|
|
221
|
+
let r = tabs + '<span style="color:rgba(100,200,150,0.7)">' + marker + '</span>'
|
|
222
|
+
if (task) {
|
|
223
|
+
r += '<span style="color:rgba(100,200,150,0.7)">' + task + '</span>'
|
|
224
|
+
}
|
|
225
|
+
return r
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// Ordered list markers
|
|
229
|
+
result = result.replace(/^(\t*)(\d+\. )/, (_, tabs, marker) =>
|
|
230
|
+
tabs + '<span style="color:rgba(100,200,150,0.7)">' + marker + '</span>')
|
|
231
|
+
|
|
232
|
+
// Images  and Links [text](url)
|
|
233
|
+
result = result.replace(/(!?\[)(.*?)(\]\()(.+?)(\))/g,
|
|
234
|
+
'<span style="color:rgba(100,180,220,0.5)">$1</span><span style="color:rgba(100,180,220,0.8)">$2</span><span style="color:rgba(100,180,220,0.5)">$3</span><span style="color:rgba(100,180,220,0.6)">$4</span><span style="color:rgba(100,180,220,0.5)">$5</span>')
|
|
235
|
+
|
|
236
|
+
// Reference links [text][ref]
|
|
237
|
+
result = result.replace(/(\[)(.*?)(\]\[)(.*?)(\])/g,
|
|
238
|
+
'<span style="color:rgba(100,180,220,0.5)">$1</span><span style="color:rgba(100,180,220,0.8)">$2</span><span style="color:rgba(100,180,220,0.5)">$3</span><span style="color:rgba(100,180,220,0.6)">$4</span><span style="color:rgba(100,180,220,0.5)">$5</span>')
|
|
239
|
+
|
|
240
|
+
// Strikethrough ~~text~~
|
|
241
|
+
result = result.replace(/(~~)(.*?)(~~)/g,
|
|
242
|
+
'<span style="color:rgba(255,100,100,0.5)">$1</span><span style="color:rgba(255,100,100,0.7)">$2</span><span style="color:rgba(255,100,100,0.5)">$3</span>')
|
|
243
|
+
|
|
244
|
+
// Bold **text**
|
|
245
|
+
result = result.replace(/(\*\*)(.*?)(\*\*)/g,
|
|
246
|
+
'<span style="color:rgba(255,180,80,0.5)">$1</span><span style="color:rgba(255,180,80,0.8)">$2</span><span style="color:rgba(255,180,80,0.5)">$3</span>')
|
|
247
|
+
|
|
248
|
+
// Italic _text_
|
|
249
|
+
result = result.replace(/((?:^|[^\\]))(\_)(.*?[^\\])(\_)/g,
|
|
250
|
+
'$1<span style="color:rgba(180,130,255,0.5)">$2</span><span style="color:rgba(180,130,255,0.8)">$3</span><span style="color:rgba(180,130,255,0.5)">$4</span>')
|
|
251
|
+
|
|
252
|
+
// HTML tags
|
|
253
|
+
result = result.replace(/(<)(\/?[a-zA-Z]\w*)(.*?)(>)/g,
|
|
254
|
+
'<span style="color:rgba(200,120,120,0.5)">$1$2$3$4</span>')
|
|
255
|
+
|
|
256
|
+
return result
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
toggleWrapMode() {
|
|
260
|
+
this.wrapEnabled = !this.wrapEnabled
|
|
261
|
+
const wrap = this.wrapEnabled
|
|
262
|
+
this.element.style.whiteSpace = wrap ? 'pre-wrap' : 'pre'
|
|
263
|
+
this.element.style.overflowX = wrap ? 'hidden' : 'auto'
|
|
264
|
+
this.highlightLayer.style.whiteSpace = wrap ? 'pre-wrap' : 'pre'
|
|
265
|
+
this.highlightLayer.style.overflowWrap = wrap ? 'break-word' : 'normal'
|
|
266
|
+
this.wrapButton.style.opacity = wrap ? '0.9' : '0.4'
|
|
267
|
+
this.updateHighlight()
|
|
268
|
+
}
|
|
269
|
+
|
|
132
270
|
escapeHtml(str) {
|
|
133
271
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
134
272
|
}
|