cm-md-editor 2.0.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.
- package/index.html +65 -17
- package/package.json +1 -1
- package/src/MdEditor.js +266 -33
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>
|
|
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 -
|
|
15
|
+
height: calc(100vh - 5rem);
|
|
13
16
|
padding: 0.7rem;
|
|
14
17
|
font-family: monospace;
|
|
15
18
|
tab-size: 4;
|
|
@@ -17,27 +20,72 @@
|
|
|
17
20
|
}
|
|
18
21
|
</style>
|
|
19
22
|
</head>
|
|
20
|
-
<body
|
|
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">
|
|
24
|
-
|
|
27
|
+
---
|
|
28
|
+
title: cm-md-editor
|
|
29
|
+
description: Some example text for the editor
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# Heading 1
|
|
33
|
+
|
|
34
|
+
## Heading 2
|
|
35
|
+
|
|
36
|
+
### Heading 3
|
|
37
|
+
|
|
38
|
+
This is a minimal **markdown editor** with _italic_ and **_bold italic_** text. You can also use ~~strikethrough~~ for deleted text.
|
|
39
|
+
|
|
40
|
+
<!-- This is a comment -->
|
|
41
|
+
|
|
42
|
+
<span>Some HTML</span>
|
|
43
|
+
|
|
44
|
+
Inline `code spans` are highlighted and protect their content: `**not bold**`.
|
|
45
|
+
|
|
46
|
+
> Blockquotes are highlighted with a green prefix.
|
|
47
|
+
|
|
48
|
+
### Links and images
|
|
49
|
+
|
|
50
|
+
Visit [the demo page](https://shaack.com/projekte/cm-md-editor/) or use a [reference link][ref].
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
[ref]: https://shaack.com
|
|
55
|
+
|
|
56
|
+
### Lists
|
|
57
|
+
|
|
58
|
+
- Unordered list item
|
|
59
|
+
- Another item
|
|
60
|
+
- Nested item
|
|
61
|
+
- Deeper nesting
|
|
62
|
+
|
|
63
|
+
1. Ordered list item
|
|
64
|
+
2. Second item
|
|
65
|
+
|
|
66
|
+
### Task lists
|
|
67
|
+
|
|
68
|
+
- [ ] Unchecked task
|
|
69
|
+
- [x] Completed task
|
|
70
|
+
|
|
71
|
+
### Code block
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
const editor = new MdEditor(element)
|
|
75
|
+
console.log("syntax highlighted")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Horizontal rules
|
|
79
|
+
|
|
80
|
+
---
|
|
25
81
|
|
|
26
|
-
|
|
82
|
+
### Escape sequences
|
|
27
83
|
|
|
28
|
-
|
|
84
|
+
Literal stars: \*not bold\* and backslash: \\
|
|
29
85
|
|
|
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
|
|
86
|
+
### HTML tags
|
|
39
87
|
|
|
40
|
-
|
|
88
|
+
This has <mark>inline HTML</mark> and a <br> tag.</textarea>
|
|
41
89
|
</div>
|
|
42
90
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
|
43
91
|
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
package/package.json
CHANGED
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
|
|
23
|
-
{title: 'Heading 1', icon: '<
|
|
24
|
-
{title: 'Heading 2', icon: '<
|
|
25
|
-
{title: 'Heading 3', icon: '<
|
|
26
|
-
{title: 'Bold', icon: '<
|
|
27
|
-
{title: 'Italic', icon: '<
|
|
28
|
-
{title: 'Unordered List', icon: '<
|
|
29
|
-
{title: 'Ordered List', icon: '<
|
|
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="
|
|
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) => {
|
|
@@ -45,9 +66,32 @@ export class MdEditor {
|
|
|
45
66
|
})
|
|
46
67
|
toolbar.appendChild(button)
|
|
47
68
|
})
|
|
69
|
+
|
|
70
|
+
// Spacer to push wrap toggle to the right
|
|
71
|
+
const spacer = document.createElement('div')
|
|
72
|
+
spacer.style.cssText = 'flex:1;'
|
|
73
|
+
toolbar.appendChild(spacer)
|
|
74
|
+
|
|
75
|
+
// Wrap toggle button
|
|
76
|
+
this.wrapEnabled = true
|
|
77
|
+
this.wrapButton = document.createElement('button')
|
|
78
|
+
this.wrapButton.type = 'button'
|
|
79
|
+
this.wrapButton.title = 'Toggle word wrap'
|
|
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;'
|
|
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>`
|
|
82
|
+
this.wrapButton.addEventListener('mouseenter', () => { this.wrapButton.style.opacity = '1'; this.wrapButton.style.background = 'rgba(128,128,128,0.2)' })
|
|
83
|
+
this.wrapButton.addEventListener('mouseleave', () => { this.wrapButton.style.opacity = this.wrapEnabled ? '0.9' : '0.4'; this.wrapButton.style.background = 'none' })
|
|
84
|
+
this.wrapButton.addEventListener('mousedown', (e) => e.preventDefault())
|
|
85
|
+
this.wrapButton.addEventListener('click', (e) => {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
this.toggleWrapMode()
|
|
88
|
+
})
|
|
89
|
+
this.wrapButton.style.opacity = '0.9'
|
|
90
|
+
toolbar.appendChild(this.wrapButton)
|
|
48
91
|
}
|
|
49
92
|
|
|
50
93
|
createHighlightBackdrop() {
|
|
94
|
+
if (!this.props.syntaxHighlight) return
|
|
51
95
|
const container = this.element.parentNode
|
|
52
96
|
container.style.position = 'relative'
|
|
53
97
|
this.backdrop = document.createElement('div')
|
|
@@ -57,7 +101,7 @@ export class MdEditor {
|
|
|
57
101
|
|
|
58
102
|
// Copy textarea computed styles to backdrop
|
|
59
103
|
const cs = window.getComputedStyle(this.element)
|
|
60
|
-
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};`
|
|
61
105
|
this.highlightLayer.style.cssText = `white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;color:transparent;`
|
|
62
106
|
|
|
63
107
|
const syncStyles = () => {
|
|
@@ -78,13 +122,15 @@ export class MdEditor {
|
|
|
78
122
|
syncStyles()
|
|
79
123
|
|
|
80
124
|
// Make textarea background transparent so backdrop shows through
|
|
125
|
+
this.element.style.overscrollBehavior = 'none'
|
|
81
126
|
this.element.style.background = 'transparent'
|
|
82
127
|
this.element.style.position = 'relative'
|
|
83
128
|
this.element.style.caretColor = cs.color
|
|
84
129
|
|
|
85
130
|
// Sync scroll
|
|
86
131
|
this.element.addEventListener('scroll', () => {
|
|
87
|
-
this.
|
|
132
|
+
this.backdrop.scrollTop = this.element.scrollTop
|
|
133
|
+
this.backdrop.scrollLeft = this.element.scrollLeft
|
|
88
134
|
})
|
|
89
135
|
|
|
90
136
|
// Update on input
|
|
@@ -93,42 +139,201 @@ export class MdEditor {
|
|
|
93
139
|
this.updateHighlight()
|
|
94
140
|
}
|
|
95
141
|
|
|
142
|
+
colorSpan(colorProp, content) {
|
|
143
|
+
return '<span style="color:rgba(' + this.props[colorProp] + ',1)">' + content + '</span>'
|
|
144
|
+
}
|
|
145
|
+
|
|
96
146
|
updateHighlight() {
|
|
97
147
|
const text = this.element.value
|
|
98
148
|
const lines = text.split('\n')
|
|
99
149
|
let html = ''
|
|
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
|
+
}
|
|
157
|
+
|
|
100
158
|
for (let i = 0; i < lines.length; i++) {
|
|
101
159
|
if (i > 0) html += '\n'
|
|
102
|
-
|
|
103
|
-
|
|
160
|
+
const line = lines[i]
|
|
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
|
+
|
|
171
|
+
// Fenced code block delimiter
|
|
172
|
+
if (/^`{3,}/.test(line)) {
|
|
173
|
+
inCodeBlock = !inCodeBlock
|
|
174
|
+
html += this.colorSpan('colorCode', this.escapeHtml(line))
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
if (inCodeBlock) {
|
|
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
|
+
}
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
104
206
|
|
|
105
|
-
//
|
|
207
|
+
// Horizontal rule (3+ of same -, *, or _ with optional spaces)
|
|
208
|
+
if (/^\s{0,3}([-*_])\s*(\1\s*){2,}$/.test(line)) {
|
|
209
|
+
html += this.colorSpan('colorHorizontalRule', this.escapeHtml(line))
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Headings
|
|
106
214
|
const headingMatch = line.match(/^(#{1,6}) /)
|
|
107
215
|
if (headingMatch) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
result = `<span style="color:rgba(100,160,255,${opacity})">${hashes} </span><span style="color:rgba(100,160,255,${opacity})">${rest}</span>`
|
|
112
|
-
} else {
|
|
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>')
|
|
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>'
|
|
218
|
+
continue
|
|
125
219
|
}
|
|
126
|
-
|
|
220
|
+
|
|
221
|
+
// Reference link definition [ref]: url
|
|
222
|
+
const refMatch = line.match(/^(\s{0,3}\[)([^\]]+)(\]:\s+)(.+)$/)
|
|
223
|
+
if (refMatch) {
|
|
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]))
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Blockquote prefix
|
|
232
|
+
let prefix = ''
|
|
233
|
+
let rest = line
|
|
234
|
+
const bqMatch = line.match(/^(\s*>+\s?)/)
|
|
235
|
+
if (bqMatch) {
|
|
236
|
+
prefix = this.colorSpan('colorBlockquote', this.escapeHtml(bqMatch[0]))
|
|
237
|
+
rest = line.substring(bqMatch[0].length)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
html += prefix + this.highlightInline(rest)
|
|
127
241
|
}
|
|
128
242
|
// Trailing newline so the backdrop height matches the textarea
|
|
129
243
|
this.highlightLayer.innerHTML = html + '\n'
|
|
130
244
|
}
|
|
131
245
|
|
|
246
|
+
highlightInline(line) {
|
|
247
|
+
// Split by inline code to protect code content from other highlighting
|
|
248
|
+
const segments = []
|
|
249
|
+
let lastIndex = 0
|
|
250
|
+
const codeRegex = /`([^`]+)`/g
|
|
251
|
+
let match
|
|
252
|
+
|
|
253
|
+
while ((match = codeRegex.exec(line)) !== null) {
|
|
254
|
+
if (match.index > lastIndex) {
|
|
255
|
+
segments.push({type: 'text', content: line.substring(lastIndex, match.index)})
|
|
256
|
+
}
|
|
257
|
+
segments.push({type: 'code', content: match[0]})
|
|
258
|
+
lastIndex = codeRegex.lastIndex
|
|
259
|
+
}
|
|
260
|
+
if (lastIndex < line.length) {
|
|
261
|
+
segments.push({type: 'text', content: line.substring(lastIndex)})
|
|
262
|
+
}
|
|
263
|
+
if (segments.length === 0) {
|
|
264
|
+
segments.push({type: 'text', content: ''})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let result = ''
|
|
268
|
+
for (const seg of segments) {
|
|
269
|
+
if (seg.type === 'code') {
|
|
270
|
+
result += this.colorSpan('colorCode', this.escapeHtml(seg.content))
|
|
271
|
+
} else {
|
|
272
|
+
result += this.highlightTextSegment(this.escapeHtml(seg.content))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return result
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
highlightTextSegment(escaped) {
|
|
279
|
+
let result = escaped
|
|
280
|
+
const c = (prop) => this.props[prop]
|
|
281
|
+
|
|
282
|
+
// Escape sequences: dim the backslash before markdown punctuation
|
|
283
|
+
result = result.replace(/\\([\\`*_{}[\]()#+\-.!~|])/g,
|
|
284
|
+
'<span style="color:rgba(' + c('colorEscape') + ',1)">\\</span>$1')
|
|
285
|
+
|
|
286
|
+
// Unordered list markers with optional task list checkbox
|
|
287
|
+
result = result.replace(/^(\t*)(- )(\[[ xX]\] )?/, (_, tabs, marker, task) => {
|
|
288
|
+
let r = tabs + this.colorSpan('colorList', marker)
|
|
289
|
+
if (task) {
|
|
290
|
+
r += this.colorSpan('colorList', task)
|
|
291
|
+
}
|
|
292
|
+
return r
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// Ordered list markers
|
|
296
|
+
result = result.replace(/^(\t*)(\d+\. )/, (_, tabs, marker) =>
|
|
297
|
+
tabs + this.colorSpan('colorList', marker))
|
|
298
|
+
|
|
299
|
+
// Images  and Links [text](url)
|
|
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))
|
|
302
|
+
|
|
303
|
+
// Reference links [text][ref]
|
|
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))
|
|
306
|
+
|
|
307
|
+
// Strikethrough ~~text~~
|
|
308
|
+
result = result.replace(/(~~)(.*?)(~~)/g, (_, p1, p2, p3) =>
|
|
309
|
+
this.colorSpan('colorStrikethrough', p1) + this.colorSpan('colorStrikethrough', p2) + this.colorSpan('colorStrikethrough', p3))
|
|
310
|
+
|
|
311
|
+
// Bold **text**
|
|
312
|
+
result = result.replace(/(\*\*)(.*?)(\*\*)/g, (_, p1, p2, p3) =>
|
|
313
|
+
this.colorSpan('colorBold', p1) + this.colorSpan('colorBold', p2) + this.colorSpan('colorBold', p3))
|
|
314
|
+
|
|
315
|
+
// Italic _text_
|
|
316
|
+
result = result.replace(/((?:^|[^\\]))(\_)(.*?[^\\])(\_)/g, (_, pre, p1, p2, p3) =>
|
|
317
|
+
pre + this.colorSpan('colorItalic', p1) + this.colorSpan('colorItalic', p2) + this.colorSpan('colorItalic', p3))
|
|
318
|
+
|
|
319
|
+
// HTML tags
|
|
320
|
+
result = result.replace(/(<)(\/?[a-zA-Z]\w*)(.*?)(>)/g, (_, p1, p2, p3, p4) =>
|
|
321
|
+
this.colorSpan('colorHtmlTag', p1 + p2 + p3 + p4))
|
|
322
|
+
|
|
323
|
+
return result
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
toggleWrapMode() {
|
|
327
|
+
this.wrapEnabled = !this.wrapEnabled
|
|
328
|
+
const wrap = this.wrapEnabled
|
|
329
|
+
this.element.style.whiteSpace = wrap ? 'pre-wrap' : 'pre'
|
|
330
|
+
this.element.style.overflowX = wrap ? 'hidden' : 'auto'
|
|
331
|
+
this.highlightLayer.style.whiteSpace = wrap ? 'pre-wrap' : 'pre'
|
|
332
|
+
this.highlightLayer.style.overflowWrap = wrap ? 'break-word' : 'normal'
|
|
333
|
+
this.wrapButton.style.opacity = wrap ? '0.9' : '0.4'
|
|
334
|
+
this.updateHighlight()
|
|
335
|
+
}
|
|
336
|
+
|
|
132
337
|
escapeHtml(str) {
|
|
133
338
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
134
339
|
}
|
|
@@ -217,6 +422,34 @@ export class MdEditor {
|
|
|
217
422
|
}
|
|
218
423
|
}
|
|
219
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('')
|
|
449
|
+
this.element.selectionStart = start + 2
|
|
450
|
+
this.element.selectionEnd = start + 2 + altText.length
|
|
451
|
+
}
|
|
452
|
+
|
|
220
453
|
insertTextAtCursor(text) {
|
|
221
454
|
// execCommand is deprecated, but without alternative to insert text and preserve the correct undo/redo stack
|
|
222
455
|
document.execCommand("insertText", false, text)
|