cm-md-editor 2.1.0 → 2.2.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.
Files changed (3) hide show
  1. package/index.html +16 -4
  2. package/package.json +1 -1
  3. package/src/MdEditor.js +134 -39
package/index.html CHANGED
@@ -3,13 +3,16 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>extreme-minimal-md-editor</title>
6
+ <title>cm-md-editor</title>
7
7
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
8
8
  integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
9
9
  <style>
10
+ body {
11
+ padding: 1rem; margin-top: 0rem;
12
+ }
10
13
  #editor {
11
14
  width: 100%;
12
- height: calc(100vh - 6rem);
15
+ height: calc(100vh - 5rem);
13
16
  padding: 0.7rem;
14
17
  font-family: monospace;
15
18
  tab-size: 4;
@@ -17,10 +20,15 @@
17
20
  }
18
21
  </style>
19
22
  </head>
20
- <body class="p-2">
23
+ <body>
21
24
  <div class="container-fluid">
22
- <label for="editor">cm-md-editor</label>
25
+ <label for="editor" style="display: none">cm-md-editor</label>
23
26
  <textarea id="editor">
27
+ ---
28
+ title: cm-md-editor
29
+ description: Some example text for the editor
30
+ ---
31
+
24
32
  # Heading 1
25
33
 
26
34
  ## Heading 2
@@ -29,6 +37,10 @@
29
37
 
30
38
  This is a minimal **markdown editor** with _italic_ and **_bold italic_** text. You can also use ~~strikethrough~~ for deleted text.
31
39
 
40
+ <!-- This is a comment -->
41
+
42
+ <span>Some HTML</span>
43
+
32
44
  Inline `code spans` are highlighted and protect their content: `**not bold**`.
33
45
 
34
46
  > Blockquotes are highlighted with a green prefix.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cm-md-editor",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "a simple markdown editor",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/MdEditor.js CHANGED
@@ -4,8 +4,26 @@
4
4
  */
5
5
  export class MdEditor {
6
6
 
7
- constructor(element) {
7
+ constructor(element, props) {
8
8
  this.element = element
9
+ this.props = {
10
+ syntaxHighlight: 1, // opacity of the highlighting, set 0 to disable
11
+ colorHeading: "100,160,255",
12
+ colorCode: "130,170,200",
13
+ colorComment: "128,128,128",
14
+ colorLink: "100,180,220",
15
+ colorBlockquote: "100,200,150",
16
+ colorList: "100,200,150",
17
+ colorStrikethrough: "255,100,100",
18
+ colorBold: "255,180,80",
19
+ colorItalic: "180,130,255",
20
+ colorHtmlTag: "200,120,120",
21
+ colorHorizontalRule: "128,128,200",
22
+ colorEscape: "128,128,128",
23
+ colorFrontMatter: "128,128,200",
24
+ toolbarButtons: ["h1", "h2", "h3", "bold", "italic", "ul", "ol", "link", "image"],
25
+ ...props
26
+ }
9
27
  this.element.addEventListener('keydown', (e) => this.handleKeyDown(e))
10
28
  this.createToolbar()
11
29
  this.createHighlightBackdrop()
@@ -19,21 +37,24 @@ export class MdEditor {
19
37
  toolbar.style.cssText = 'display:flex;gap:1px;padding:2px;flex-wrap:wrap;background:rgba(128,128,128,0.15);border:1px solid rgba(128,128,128,0.3);border-bottom:none;border-radius:4px 4px 0 0;box-sizing:border-box;width:100%;'
20
38
  wrapper.insertBefore(toolbar, this.element)
21
39
  this.element.style.borderRadius = '0 0 4px 4px'
22
- const buttons = [
23
- {title: 'Heading 1', icon: '<text x="12" y="17.5" font-size="16" font-family="system-ui,sans-serif" font-weight="700" text-anchor="middle">H1</text>', action: () => this.toggleHeading(1)},
24
- {title: 'Heading 2', icon: '<text x="12" y="17.5" font-size="16" font-family="system-ui,sans-serif" font-weight="700" text-anchor="middle">H2</text>', action: () => this.toggleHeading(2)},
25
- {title: 'Heading 3', icon: '<text x="12" y="17.5" font-size="16" font-family="system-ui,sans-serif" font-weight="700" text-anchor="middle">H3</text>', action: () => this.toggleHeading(3)},
26
- {title: 'Bold', icon: '<text x="12" y="18" font-size="18" font-family="system-ui,sans-serif" font-weight="800" text-anchor="middle">B</text>', action: () => this.toggleBold()},
27
- {title: 'Italic', icon: '<text x="12" y="18" font-size="18" font-family="system-ui,sans-serif" font-weight="600" font-style="italic" text-anchor="middle">I</text>', action: () => this.toggleItalic()},
28
- {title: 'Unordered List', icon: '<circle cx="5" cy="7" r="1.8"/><circle cx="5" cy="17" r="1.8"/><rect x="9.5" y="5.5" width="11" height="3" rx="1"/><rect x="9.5" y="15.5" width="11" height="3" rx="1"/>', action: () => this.insertUnorderedList()},
29
- {title: 'Ordered List', icon: '<text x="3" y="9.5" font-size="8.5" font-family="system-ui,sans-serif" font-weight="700">1</text><text x="3" y="19.5" font-size="8.5" font-family="system-ui,sans-serif" font-weight="700">2</text><rect x="9.5" y="5.5" width="11" height="3" rx="1"/><rect x="9.5" y="15.5" width="11" height="3" rx="1"/>', action: () => this.insertOrderedList()},
40
+ const allButtons = [
41
+ {name: 'h1', title: 'Heading 1', icon: '<path d="M7.648 13V3H6.3v4.234H1.348V3H0v10h1.348V8.421H6.3V13zM14 13V3h-1.333l-2.381 1.766V6.12L12.6 4.443h.066V13z"/>', action: () => this.toggleHeading(1)},
42
+ {name: 'h2', title: 'Heading 2', icon: '<path d="M7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13zm3.174-7.071v-.05c0-.934.66-1.752 1.801-1.752 1.005 0 1.76.639 1.76 1.651 0 .898-.582 1.58-1.12 2.19l-3.69 4.2V13h6.331v-1.149h-4.458v-.079L13.9 8.786c.919-1.048 1.666-1.874 1.666-3.101C15.565 4.149 14.35 3 12.499 3 10.46 3 9.384 4.393 9.384 5.879v.05z"/>', action: () => this.toggleHeading(2)},
43
+ {name: 'h3', title: 'Heading 3', icon: '<path d="M11.07 8.4h1.049c1.174 0 1.99.69 2.004 1.724s-.802 1.786-2.068 1.779c-1.11-.007-1.905-.605-1.99-1.357h-1.21C8.926 11.91 10.116 13 12.028 13c1.99 0 3.439-1.188 3.404-2.87-.028-1.553-1.287-2.221-2.096-2.313v-.07c.724-.127 1.814-.935 1.772-2.293-.035-1.392-1.21-2.468-3.038-2.454-1.927.007-2.94 1.196-2.981 2.426h1.23c.064-.71.732-1.336 1.744-1.336 1.027 0 1.744.64 1.744 1.568.007.95-.738 1.639-1.744 1.639h-.991V8.4ZM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/>', action: () => this.toggleHeading(3)},
44
+ {name: 'bold', title: 'Bold', icon: '<path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z"/>', action: () => this.toggleBold()},
45
+ {name: 'italic', title: 'Italic', icon: '<path d="M7.991 11.674 9.53 4.455c.123-.595.246-.71 1.347-.807l.11-.52H7.211l-.11.52c1.06.096 1.128.212 1.005.807L6.57 11.674c-.123.595-.246.71-1.346.806l-.11.52h3.774l.11-.52c-1.06-.095-1.129-.211-1.006-.806z"/>', action: () => this.toggleItalic()},
46
+ {name: 'ul', title: 'Unordered List', icon: '<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>', action: () => this.insertUnorderedList()},
47
+ {name: 'ol', title: 'Ordered List', icon: '<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5"/><path d="M1.713 11.865v-.474H2c.217 0 .363-.137.363-.317 0-.185-.158-.31-.361-.31-.223 0-.367.152-.373.31h-.59c.016-.467.373-.787.986-.787.588-.002.954.291.957.703a.595.595 0 0 1-.492.594v.033a.615.615 0 0 1 .569.631c.003.533-.502.8-1.051.8-.656 0-1-.37-1.008-.794h.582c.008.178.186.306.422.309.254 0 .424-.145.422-.35-.002-.195-.155-.348-.414-.348h-.3zm-.004-4.699h-.604v-.035c0-.408.295-.844.958-.844.583 0 .96.326.96.756 0 .389-.257.617-.476.848l-.537.572v.03h1.054V9H1.143v-.395l.957-.99c.138-.142.293-.304.293-.508 0-.18-.147-.32-.342-.32a.33.33 0 0 0-.342.338zM2.564 5h-.635V2.924h-.031l-.598.42v-.567l.629-.443h.635z"/>', action: () => this.insertOrderedList()},
48
+ {name: 'link', title: 'Insert Link', icon: '<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/><path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>', action: () => this.insertLink()},
49
+ {name: 'image', title: 'Insert Image', icon: '<path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/><path d="M1.5 2A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2zm13 1a.5.5 0 0 1 .5.5v6l-3.775-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12v.54L1 12.5v-9a.5.5 0 0 1 .5-.5z"/>', action: () => this.insertImage()},
30
50
  ]
51
+ const buttons = allButtons.filter(btn => this.props.toolbarButtons.includes(btn.name))
31
52
  buttons.forEach(btn => {
32
53
  const button = document.createElement('button')
33
54
  button.type = 'button'
34
55
  button.title = btn.title
35
56
  button.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;'
36
- button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor">${btn.icon}</svg>`
57
+ button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 16 16" fill="currentColor">${btn.icon}</svg>`
37
58
  button.addEventListener('mouseenter', () => { button.style.opacity = '1'; button.style.background = 'rgba(128,128,128,0.2)' })
38
59
  button.addEventListener('mouseleave', () => { button.style.opacity = '0.6'; button.style.background = 'none' })
39
60
  button.addEventListener('mousedown', (e) => {
@@ -57,7 +78,7 @@ export class MdEditor {
57
78
  this.wrapButton.type = 'button'
58
79
  this.wrapButton.title = 'Toggle word wrap'
59
80
  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>`
81
+ this.wrapButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 16 16" fill="currentColor"><path fill-rule="evenodd" d="M2 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5m0 4a.5.5 0 0 1 .5-.5h9a2.5 2.5 0 0 1 0 5h-1.293l.647.646a.5.5 0 0 1-.708.708l-1.5-1.5a.5.5 0 0 1 0-.708l1.5-1.5a.5.5 0 0 1 .708.708l-.647.646H11.5a1.5 1.5 0 0 0 0-3h-9a.5.5 0 0 1-.5-.5m0 4a.5.5 0 0 1 .5-.5H7a.5.5 0 0 1 0 1H2.5a.5.5 0 0 1-.5-.5"/></svg>`
61
82
  this.wrapButton.addEventListener('mouseenter', () => { this.wrapButton.style.opacity = '1'; this.wrapButton.style.background = 'rgba(128,128,128,0.2)' })
62
83
  this.wrapButton.addEventListener('mouseleave', () => { this.wrapButton.style.opacity = this.wrapEnabled ? '0.9' : '0.4'; this.wrapButton.style.background = 'none' })
63
84
  this.wrapButton.addEventListener('mousedown', (e) => e.preventDefault())
@@ -70,6 +91,7 @@ export class MdEditor {
70
91
  }
71
92
 
72
93
  createHighlightBackdrop() {
94
+ if (!this.props.syntaxHighlight) return
73
95
  const container = this.element.parentNode
74
96
  container.style.position = 'relative'
75
97
  this.backdrop = document.createElement('div')
@@ -79,7 +101,7 @@ export class MdEditor {
79
101
 
80
102
  // Copy textarea computed styles to backdrop
81
103
  const cs = window.getComputedStyle(this.element)
82
- this.backdrop.style.cssText = `position:absolute;overflow:hidden;pointer-events:none;z-index:1;`
104
+ this.backdrop.style.cssText = `position:absolute;overflow:hidden;pointer-events:none;z-index:1;opacity:${this.props.syntaxHighlight};`
83
105
  this.highlightLayer.style.cssText = `white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;color:transparent;`
84
106
 
85
107
  const syncStyles = () => {
@@ -117,48 +139,92 @@ export class MdEditor {
117
139
  this.updateHighlight()
118
140
  }
119
141
 
142
+ colorSpan(colorProp, content) {
143
+ return '<span style="color:rgba(' + this.props[colorProp] + ',1)">' + content + '</span>'
144
+ }
145
+
120
146
  updateHighlight() {
121
147
  const text = this.element.value
122
148
  const lines = text.split('\n')
123
149
  let html = ''
124
150
  let inCodeBlock = false
151
+ let inHtmlComment = false
152
+ let inFrontMatter = false
153
+ // YAML front matter must start at the very first line
154
+ if (lines.length > 0 && lines[0].trim() === '---') {
155
+ inFrontMatter = true
156
+ }
125
157
 
126
158
  for (let i = 0; i < lines.length; i++) {
127
159
  if (i > 0) html += '\n'
128
160
  const line = lines[i]
129
161
 
162
+ // YAML front matter
163
+ if (inFrontMatter) {
164
+ html += this.colorSpan('colorFrontMatter', this.escapeHtml(line))
165
+ if (i > 0 && line.trim() === '---') {
166
+ inFrontMatter = false
167
+ }
168
+ continue
169
+ }
170
+
130
171
  // Fenced code block delimiter
131
172
  if (/^`{3,}/.test(line)) {
132
173
  inCodeBlock = !inCodeBlock
133
- html += '<span style="color:rgba(130,170,200,0.7)">' + this.escapeHtml(line) + '</span>'
174
+ html += this.colorSpan('colorCode', this.escapeHtml(line))
134
175
  continue
135
176
  }
136
177
  if (inCodeBlock) {
137
- html += '<span style="color:rgba(130,170,200,0.5)">' + this.escapeHtml(line) + '</span>'
178
+ html += this.colorSpan('colorCode', this.escapeHtml(line))
179
+ continue
180
+ }
181
+
182
+ // HTML comment handling (can span multiple lines)
183
+ if (inHtmlComment) {
184
+ const endIdx = line.indexOf('-->')
185
+ if (endIdx !== -1) {
186
+ inHtmlComment = false
187
+ html += this.colorSpan('colorComment', this.escapeHtml(line.substring(0, endIdx + 3)))
188
+ html += this.highlightInline(line.substring(endIdx + 3))
189
+ } else {
190
+ html += this.colorSpan('colorComment', this.escapeHtml(line))
191
+ }
192
+ continue
193
+ }
194
+ if (line.trimStart().startsWith('<!--')) {
195
+ const endIdx = line.indexOf('-->', line.indexOf('<!--') + 4)
196
+ if (endIdx !== -1) {
197
+ // Single-line comment
198
+ html += this.colorSpan('colorComment', this.escapeHtml(line))
199
+ } else {
200
+ // Multi-line comment starts
201
+ inHtmlComment = true
202
+ html += this.colorSpan('colorComment', this.escapeHtml(line))
203
+ }
138
204
  continue
139
205
  }
140
206
 
141
207
  // Horizontal rule (3+ of same -, *, or _ with optional spaces)
142
208
  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>'
209
+ html += this.colorSpan('colorHorizontalRule', this.escapeHtml(line))
144
210
  continue
145
211
  }
146
212
 
147
213
  // Headings
148
214
  const headingMatch = line.match(/^(#{1,6}) /)
149
215
  if (headingMatch) {
150
- const opacity = Math.max(0.3, 0.8 - (headingMatch[1].length - 1) * 0.1)
151
- html += '<span style="color:rgba(100,160,255,' + opacity + ')">' + this.escapeHtml(line) + '</span>'
216
+ const opacity = Math.max(0.3, 1 - (headingMatch[1].length - 1) * 0.1)
217
+ html += '<span style="color:rgba(' + this.props.colorHeading + ',' + opacity + ')">' + this.escapeHtml(line) + '</span>'
152
218
  continue
153
219
  }
154
220
 
155
221
  // Reference link definition [ref]: url
156
222
  const refMatch = line.match(/^(\s{0,3}\[)([^\]]+)(\]:\s+)(.+)$/)
157
223
  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>'
224
+ html += this.colorSpan('colorLink', this.escapeHtml(refMatch[1]))
225
+ + this.colorSpan('colorLink', this.escapeHtml(refMatch[2]))
226
+ + this.colorSpan('colorLink', this.escapeHtml(refMatch[3]))
227
+ + this.colorSpan('colorLink', this.escapeHtml(refMatch[4]))
162
228
  continue
163
229
  }
164
230
 
@@ -167,7 +233,7 @@ export class MdEditor {
167
233
  let rest = line
168
234
  const bqMatch = line.match(/^(\s*>+\s?)/)
169
235
  if (bqMatch) {
170
- prefix = '<span style="color:rgba(128,180,128,0.7)">' + this.escapeHtml(bqMatch[0]) + '</span>'
236
+ prefix = this.colorSpan('colorBlockquote', this.escapeHtml(bqMatch[0]))
171
237
  rest = line.substring(bqMatch[0].length)
172
238
  }
173
239
 
@@ -201,7 +267,7 @@ export class MdEditor {
201
267
  let result = ''
202
268
  for (const seg of segments) {
203
269
  if (seg.type === 'code') {
204
- result += '<span style="color:rgba(130,170,200,0.7)">' + this.escapeHtml(seg.content) + '</span>'
270
+ result += this.colorSpan('colorCode', this.escapeHtml(seg.content))
205
271
  } else {
206
272
  result += this.highlightTextSegment(this.escapeHtml(seg.content))
207
273
  }
@@ -211,47 +277,48 @@ export class MdEditor {
211
277
 
212
278
  highlightTextSegment(escaped) {
213
279
  let result = escaped
280
+ const c = (prop) => this.props[prop]
214
281
 
215
282
  // Escape sequences: dim the backslash before markdown punctuation
216
283
  result = result.replace(/\\([\\`*_{}[\]()#+\-.!~|])/g,
217
- '<span style="color:rgba(128,128,128,0.4)">\\</span>$1')
284
+ '<span style="color:rgba(' + c('colorEscape') + ',1)">\\</span>$1')
218
285
 
219
286
  // Unordered list markers with optional task list checkbox
220
287
  result = result.replace(/^(\t*)(- )(\[[ xX]\] )?/, (_, tabs, marker, task) => {
221
- let r = tabs + '<span style="color:rgba(100,200,150,0.7)">' + marker + '</span>'
288
+ let r = tabs + this.colorSpan('colorList', marker)
222
289
  if (task) {
223
- r += '<span style="color:rgba(100,200,150,0.7)">' + task + '</span>'
290
+ r += this.colorSpan('colorList', task)
224
291
  }
225
292
  return r
226
293
  })
227
294
 
228
295
  // Ordered list markers
229
296
  result = result.replace(/^(\t*)(\d+\. )/, (_, tabs, marker) =>
230
- tabs + '<span style="color:rgba(100,200,150,0.7)">' + marker + '</span>')
297
+ tabs + this.colorSpan('colorList', marker))
231
298
 
232
299
  // Images ![alt](url) 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>')
300
+ result = result.replace(/(!?\[)(.*?)(\]\()(.+?)(\))/g, (_, p1, p2, p3, p4, p5) =>
301
+ this.colorSpan('colorLink', p1) + this.colorSpan('colorLink', p2) + this.colorSpan('colorLink', p3) + this.colorSpan('colorLink', p4) + this.colorSpan('colorLink', p5))
235
302
 
236
303
  // 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>')
304
+ result = result.replace(/(\[)(.*?)(\]\[)(.*?)(\])/g, (_, p1, p2, p3, p4, p5) =>
305
+ this.colorSpan('colorLink', p1) + this.colorSpan('colorLink', p2) + this.colorSpan('colorLink', p3) + this.colorSpan('colorLink', p4) + this.colorSpan('colorLink', p5))
239
306
 
240
307
  // 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>')
308
+ result = result.replace(/(~~)(.*?)(~~)/g, (_, p1, p2, p3) =>
309
+ this.colorSpan('colorStrikethrough', p1) + this.colorSpan('colorStrikethrough', p2) + this.colorSpan('colorStrikethrough', p3))
243
310
 
244
311
  // 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>')
312
+ result = result.replace(/(\*\*)(.*?)(\*\*)/g, (_, p1, p2, p3) =>
313
+ this.colorSpan('colorBold', p1) + this.colorSpan('colorBold', p2) + this.colorSpan('colorBold', p3))
247
314
 
248
315
  // 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>')
316
+ result = result.replace(/((?:^|[^\\]))(\_)(.*?[^\\])(\_)/g, (_, pre, p1, p2, p3) =>
317
+ pre + this.colorSpan('colorItalic', p1) + this.colorSpan('colorItalic', p2) + this.colorSpan('colorItalic', p3))
251
318
 
252
319
  // HTML tags
253
- result = result.replace(/(&lt;)(\/?[a-zA-Z]\w*)(.*?)(&gt;)/g,
254
- '<span style="color:rgba(200,120,120,0.5)">$1$2$3$4</span>')
320
+ result = result.replace(/(&lt;)(\/?[a-zA-Z]\w*)(.*?)(&gt;)/g, (_, p1, p2, p3, p4) =>
321
+ this.colorSpan('colorHtmlTag', p1 + p2 + p3 + p4))
255
322
 
256
323
  return result
257
324
  }
@@ -355,6 +422,34 @@ export class MdEditor {
355
422
  }
356
423
  }
357
424
 
425
+ insertLink() {
426
+ const start = this.element.selectionStart
427
+ const end = this.element.selectionEnd
428
+ const selected = this.element.value.substring(start, end)
429
+ const url = prompt('Enter URL:')
430
+ if (url === null) return
431
+ const linkText = selected || 'link'
432
+ this.element.focus()
433
+ this.selectLineRange(start, end)
434
+ this.insertTextAtCursor('[' + linkText + '](' + url + ')')
435
+ this.element.selectionStart = start + 1
436
+ this.element.selectionEnd = start + 1 + linkText.length
437
+ }
438
+
439
+ insertImage() {
440
+ const start = this.element.selectionStart
441
+ const end = this.element.selectionEnd
442
+ const selected = this.element.value.substring(start, end)
443
+ const url = prompt('Enter image URL:')
444
+ if (url === null) return
445
+ const altText = selected || 'image'
446
+ this.element.focus()
447
+ this.selectLineRange(start, end)
448
+ this.insertTextAtCursor('![' + altText + '](' + url + ')')
449
+ this.element.selectionStart = start + 2
450
+ this.element.selectionEnd = start + 2 + altText.length
451
+ }
452
+
358
453
  insertTextAtCursor(text) {
359
454
  // execCommand is deprecated, but without alternative to insert text and preserve the correct undo/redo stack
360
455
  document.execCommand("insertText", false, text)