cm-md-editor 2.3.3 → 3.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,12 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//private/tmp/**)",
5
+ "Bash(npm pack:*)",
6
+ "Bash(for f:*)",
7
+ "Bash(do echo:*)",
8
+ "Read(//Users/shaack/Development/repositories/MyProjects/cm-md-editor/**)",
9
+ "Bash(done)"
10
+ ]
11
+ }
12
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Stefan Haack (shaack.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,15 +1,214 @@
1
1
  # cm-md-editor
2
2
 
3
- This is a minimal **markdown editor** which is used in chessmail.de.
3
+ A minimal, dependency-free markdown editor as a vanilla JavaScript ES6 module.
4
+
5
+ [Demo](https://shaack.com/projekte/cm-md-editor/)
6
+
7
+ ![Screenshot](screenshot.png)
4
8
 
5
9
  ## Key features
6
10
 
7
- - Vanilla JavaScript module, without dependencies
8
- - Supports outlines with tab and shift-tab to indent and outdent
9
- - Supports the **bold** syntax with command-b/ctrl-b
10
- - Supports the _italic_ syntax with command-i/ctrl-i
11
- - Supports undo and redo with command-z/ctrl-z and command-shift-z/ctrl-shift-z
12
- - It is…
13
- - lightweight
14
- - easy to use
15
- - fast
11
+ - Vanilla JavaScript module, zero dependencies
12
+ - Syntax highlighting for headings, bold, italic, strikethrough, code, lists, links, images, blockquotes, HTML tags, horizontal rules, front matter and more
13
+ - Modular toolbar built from composable tools
14
+ - Word wrap toggle with persistent state (localStorage)
15
+ - List mode: Tab/Shift-Tab to indent/outdent, auto-continuation on Enter
16
+ - Bold with Ctrl/Cmd+B, italic with Ctrl/Cmd+I (provided by tools)
17
+ - Native undo/redo support (Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z)
18
+ - Lightweight, fast, easy to use
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install cm-md-editor
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```html
29
+ <textarea id="editor"></textarea>
30
+
31
+ <script type="module">
32
+ import {MdEditor} from "cm-md-editor/src/MdEditor.js"
33
+
34
+ const editor = new MdEditor(document.getElementById("editor"))
35
+ </script>
36
+ ```
37
+
38
+ This creates an editor with the default toolbar: Headings (h1–h3), Bold, Italic, Strikethrough, Unordered List, Ordered List, Insert Link, Insert Image.
39
+
40
+ ### Custom toolbar
41
+
42
+ Compose your own toolbar by passing a `tools` array:
43
+
44
+ ```javascript
45
+ import {MdEditor} from "cm-md-editor/src/MdEditor.js"
46
+ import {Headings} from "cm-md-editor/src/tools/Headings.js"
47
+ import {Bold} from "cm-md-editor/src/tools/Bold.js"
48
+ import {Italic} from "cm-md-editor/src/tools/Italic.js"
49
+ import {Separator} from "cm-md-editor/src/tools/Separator.js"
50
+ import {InsertLink} from "cm-md-editor/src/tools/InsertLink.js"
51
+
52
+ new MdEditor(document.getElementById("editor"), {
53
+ tools: [Headings, Separator, Bold, Italic, Separator, InsertLink]
54
+ })
55
+ ```
56
+
57
+ ### Configuring tools
58
+
59
+ Tools that accept options can be passed as `[ToolClass, props]` tuples:
60
+
61
+ ```javascript
62
+ new MdEditor(document.getElementById("editor"), {
63
+ tools: [[Headings, {minLevel: 2, maxLevel: 4}], Bold, Italic]
64
+ })
65
+ ```
66
+
67
+ ## Configuration (props)
68
+
69
+ All props are optional. Pass them as the second argument to the constructor.
70
+
71
+ | Prop | Type | Default | Description |
72
+ |------|------|---------|-------------|
73
+ | `tools` | `array` | `defaultTools` | Array of tool classes (or `[class, props]` tuples). See [Tools](#tools) |
74
+ | `wordWrap` | `boolean` | `true` | Default word wrap state. Overridden by localStorage if the user has toggled it |
75
+ | `colorHeading` | `string` | `"100,160,255"` | RGB color for headings |
76
+ | `colorCode` | `string` | `"130,170,200"` | RGB color for code spans and fenced code blocks |
77
+ | `colorComment` | `string` | `"128,128,128"` | RGB color for HTML comments |
78
+ | `colorLink` | `string` | `"100,180,220"` | RGB color for links and images |
79
+ | `colorBlockquote` | `string` | `"100,200,150"` | RGB color for blockquote prefixes |
80
+ | `colorList` | `string` | `"100,200,150"` | RGB color for list markers |
81
+ | `colorStrikethrough` | `string` | `"255,100,100"` | RGB color for ~~strikethrough~~ |
82
+ | `colorBold` | `string` | `"255,180,80"` | RGB color for **bold** |
83
+ | `colorItalic` | `string` | `"180,130,255"` | RGB color for _italic_ |
84
+ | `colorHtmlTag` | `string` | `"200,120,120"` | RGB color for HTML tags |
85
+ | `colorHorizontalRule` | `string` | `"128,128,200"` | RGB color for horizontal rules |
86
+ | `colorEscape` | `string` | `"128,128,128"` | RGB color for escape sequences |
87
+ | `colorFrontMatter` | `string` | `"128,128,200"` | RGB color for YAML front matter |
88
+
89
+ Colors are specified as RGB strings (e.g. `"255,180,80"`) and rendered at full opacity.
90
+
91
+ ## Tools
92
+
93
+ The toolbar is built entirely from tools. Each tool is a class that provides toolbar buttons, keyboard shortcuts, and/or syntax highlighting extensions.
94
+
95
+ ### Built-in tools
96
+
97
+ | Tool | Buttons | Shortcut | Description |
98
+ |------|---------|----------|-------------|
99
+ | `Headings` | h1, h2, h3 | — | Toggle heading levels. Props: `{minLevel, maxLevel}` (defaults: 1–3) |
100
+ | `Bold` | bold | Ctrl/Cmd+B | Toggle bold (`**`) |
101
+ | `Italic` | italic | Ctrl/Cmd+I | Toggle italic (`_`) |
102
+ | `Strikethrough` | strikethrough | — | Toggle strikethrough (`~~`) |
103
+ | `UnorderedList` | ul | — | Toggle unordered list prefix (`- `) |
104
+ | `OrderedList` | ol | — | Toggle ordered list prefix (`1. `) |
105
+ | `InsertLink` | link | — | Insert markdown link |
106
+ | `InsertImage` | image | — | Insert markdown image |
107
+ | `Separator` | — | — | Visual divider in the toolbar. Can be used multiple times |
108
+
109
+ All built-in tools are exported from `src/tools/DefaultTools.js`:
110
+
111
+ ```javascript
112
+ import {defaultTools} from "cm-md-editor/src/tools/DefaultTools.js"
113
+ ```
114
+
115
+ The default toolbar order is:
116
+
117
+ ```
118
+ Headings | Bold, Italic, Strikethrough | UnorderedList, OrderedList | InsertLink, InsertImage
119
+ ```
120
+
121
+ ### Writing a custom tool
122
+
123
+ A tool is a class that receives the editor instance (and optional props) in its constructor. It can implement any combination of three optional methods:
124
+
125
+ ```javascript
126
+ export class MyTool {
127
+ constructor(editor, props = {}) {
128
+ this.editor = editor
129
+ }
130
+
131
+ // Optional: add buttons to the toolbar
132
+ toolbarButtons() {
133
+ return [{
134
+ name: 'mytool',
135
+ title: 'My Tool',
136
+ // Icon options (use one):
137
+ icon: '<path d="..."/>', // inline SVG path for a 16x16 viewBox
138
+ iconFile: 'my-icon.svg', // filename in src/tools/icons/
139
+ iconUrl: 'https://...', // full URL to an SVG file
140
+ action: () => { /* ... */ }
141
+ }]
142
+ }
143
+
144
+ // Optional: register keyboard shortcuts
145
+ keyboardShortcuts() {
146
+ return [{
147
+ key: 'e', // the key to match (KeyboardEvent.key)
148
+ ctrlOrMeta: true, // require Ctrl (Windows/Linux) or Cmd (Mac)
149
+ action: (e) => { /* ... */ }
150
+ }]
151
+ }
152
+
153
+ // Optional: extend syntax highlighting (receives already-escaped HTML)
154
+ highlightInline(html) {
155
+ return html.replace(...)
156
+ }
157
+ }
158
+ ```
159
+
160
+ For tools with co-located icons, use `import.meta.url` to resolve the icon path:
161
+
162
+ ```javascript
163
+ toolbarButtons() {
164
+ return [{
165
+ name: 'mytool',
166
+ title: 'My Tool',
167
+ iconUrl: new URL("my-icon.svg", import.meta.url).href,
168
+ action: () => { /* ... */ }
169
+ }]
170
+ }
171
+ ```
172
+
173
+ ### Editor API available to tools
174
+
175
+ These public methods and properties are available via `this.editor`:
176
+
177
+ | Method / Property | Description |
178
+ |-------------------|-------------|
179
+ | `editor.element` | The textarea element (read `value`, `selectionStart`, `selectionEnd`) |
180
+ | `editor.insertTextAtCursor(text)` | Insert text at cursor, preserving native undo/redo |
181
+ | `editor.getCurrentLineInfo()` | Returns `{lineStart, lineEnd, line}` for the current line |
182
+ | `editor.selectLineRange(start, end)` | Set the textarea selection range |
183
+ | `editor.toggleWrap(marker)` | Toggle wrapping markers around the selection (e.g. `**` for bold) |
184
+ | `editor.escapeHtml(str)` | Escape a string for use in the highlight layer |
185
+ | `editor.colorSpan(colorProp, content)` | Wrap content in a colored `<span>` using an RGB color prop |
186
+
187
+ ### Example: DummyText tool
188
+
189
+ A tool that inserts lorem ipsum text (see `example-addon-tools/DummyText.js`):
190
+
191
+ ```javascript
192
+ import {MdEditor} from "cm-md-editor/src/MdEditor.js"
193
+ import {defaultTools} from "cm-md-editor/src/tools/DefaultTools.js"
194
+ import {Separator} from "cm-md-editor/src/tools/Separator.js"
195
+ import {DummyText} from "./example-addon-tools/DummyText.js"
196
+
197
+ new MdEditor(document.getElementById("editor"), {
198
+ tools: [...defaultTools, Separator, DummyText]
199
+ })
200
+ ```
201
+
202
+ ## Keyboard shortcuts
203
+
204
+ | Shortcut | Action | Provided by |
205
+ |----------|--------|-------------|
206
+ | Ctrl/Cmd + B | Toggle bold | `Bold` tool |
207
+ | Ctrl/Cmd + I | Toggle italic | `Italic` tool |
208
+ | Tab | Indent list item or insert tab | Core editor |
209
+ | Shift + Tab | Outdent list item | Core editor |
210
+ | Enter | Auto-continue list (unordered and ordered) | Core editor |
211
+
212
+ ## License
213
+
214
+ MIT
@@ -0,0 +1,42 @@
1
+ const WORDS = [
2
+ "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
3
+ "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
4
+ "magna", "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud",
5
+ "exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo",
6
+ "consequat", "duis", "aute", "irure", "in", "reprehenderit", "voluptate",
7
+ "velit", "esse", "cillum", "fugiat", "nulla", "pariatur", "excepteur", "sint",
8
+ "occaecat", "cupidatat", "non", "proident", "sunt", "culpa", "qui", "officia",
9
+ "deserunt", "mollit", "anim", "id", "est", "laborum"
10
+ ]
11
+
12
+ function generateDummyText(wordCount) {
13
+ const result = []
14
+ for (let i = 0; i < wordCount; i++) {
15
+ result.push(WORDS[i % WORDS.length])
16
+ }
17
+ result[0] = result[0].charAt(0).toUpperCase() + result[0].slice(1)
18
+ return result.join(" ") + "."
19
+ }
20
+
21
+ export class DummyText {
22
+ constructor(editor) {
23
+ this.editor = editor
24
+ }
25
+
26
+ toolbarButtons() {
27
+ const iconUrl = new URL("bi-body-text.svg", import.meta.url).href
28
+ return [{
29
+ name: "dummy-text",
30
+ title: "Insert dummy text",
31
+ iconUrl: iconUrl,
32
+ action: () => this.insertDummyText()
33
+ }]
34
+ }
35
+
36
+ insertDummyText() {
37
+ const input = prompt("Word count (1\u2013100):", "20")
38
+ if (input === null) return
39
+ const count = Math.max(1, Math.min(100, parseInt(input) || 20))
40
+ this.editor.insertTextAtCursor(generateDummyText(count))
41
+ }
42
+ }
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-body-text" viewBox="0 0 16 16">
2
+ <path fill-rule="evenodd" d="M0 .5A.5.5 0 0 1 .5 0h4a.5.5 0 0 1 0 1h-4A.5.5 0 0 1 0 .5m0 2A.5.5 0 0 1 .5 2h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m9 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m-9 2A.5.5 0 0 1 .5 4h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5m5 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m7 0a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5m-12 2A.5.5 0 0 1 .5 6h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5m8 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m-8 2A.5.5 0 0 1 .5 8h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m7 0a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m-7 2a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5"/>
3
+ </svg>
package/index.html CHANGED
@@ -1,28 +1,13 @@
1
1
  <!doctype html>
2
- <html lang="en" data-bs-theme="dark">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>cm-md-editor</title>
7
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
8
- integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
9
- <style>
10
- body {
11
- padding: 1rem; margin-top: 0rem;
12
- }
13
- #editor {
14
- width: 100%;
15
- height: calc(100vh - 5rem);
16
- padding: 0.7rem;
17
- font-family: monospace;
18
- tab-size: 4;
19
- line-height: 1.3;
20
- background-color: #111;
21
- }
22
- </style>
7
+ <link rel="stylesheet" href="src/cm-md-editor.css">
23
8
  </head>
24
9
  <body>
25
- <div class="container-fluid">
10
+ <div>
26
11
  <label for="editor" style="display: none">cm-md-editor</label>
27
12
  <textarea id="editor">
28
13
  ---
@@ -88,13 +73,15 @@ Literal stars: \*not bold\* and backslash: \\
88
73
 
89
74
  This has <mark>inline HTML</mark> and a <br> tag.</textarea>
90
75
  </div>
91
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
92
- integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
93
- crossorigin="anonymous"></script>
94
76
  <script type="module">
95
77
  import {MdEditor} from "./src/MdEditor.js"
78
+ import {defaultTools} from "./src/tools/DefaultTools.js"
79
+ import {Separator} from "./src/tools/Separator.js"
80
+ import {DummyText} from "./example-addon-tools/DummyText.js"
96
81
 
97
- new MdEditor(document.getElementById('editor'))
82
+ new MdEditor(document.getElementById('editor'), {
83
+ tools: [...defaultTools, Separator, DummyText]
84
+ })
98
85
  </script>
99
86
  </body>
100
87
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cm-md-editor",
3
- "version": "2.3.3",
3
+ "version": "3.0.0",
4
4
  "description": "a simple markdown editor",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/screenshot.png ADDED
Binary file
package/src/MdEditor.js CHANGED
@@ -2,12 +2,13 @@
2
2
  * Author: Stefan Haack (https://shaack.com)
3
3
  * Date: 2023-11-12
4
4
  */
5
+ import {defaultTools} from "./tools/DefaultTools.js"
6
+
5
7
  export class MdEditor {
6
8
 
7
9
  constructor(element, props) {
8
10
  this.element = element
9
11
  this.props = {
10
- syntaxHighlight: 1, // opacity of the highlighting, set 0 to disable
11
12
  colorHeading: "100,160,255",
12
13
  colorCode: "130,170,200",
13
14
  colorComment: "128,128,128",
@@ -21,9 +22,17 @@ export class MdEditor {
21
22
  colorHorizontalRule: "128,128,200",
22
23
  colorEscape: "128,128,128",
23
24
  colorFrontMatter: "128,128,200",
24
- toolbarButtons: ["h1", "h2", "h3", "bold", "italic", "ul", "ol", "link", "image"],
25
+ wordWrap: true,
26
+ tools: defaultTools,
25
27
  ...props
26
28
  }
29
+ this.tools = this.props.tools.map(entry => {
30
+ if (Array.isArray(entry)) {
31
+ const [Tool, toolProps] = entry
32
+ return new Tool(this, toolProps)
33
+ }
34
+ return new entry(this)
35
+ })
27
36
  this.element.addEventListener('keydown', (e) => this.handleKeyDown(e))
28
37
  this.createToolbar()
29
38
  this.createHighlightBackdrop()
@@ -41,35 +50,11 @@ export class MdEditor {
41
50
  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%;'
42
51
  wrapper.insertBefore(toolbar, this.element)
43
52
  this.element.style.borderRadius = '0 0 4px 4px'
44
- const allButtons = [
45
- {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)},
46
- {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)},
47
- {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)},
48
- {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()},
49
- {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()},
50
- {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()},
51
- {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()},
52
- {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()},
53
- {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()},
54
- ]
55
- const buttons = allButtons.filter(btn => this.props.toolbarButtons.includes(btn.name))
56
- buttons.forEach(btn => {
57
- const button = document.createElement('button')
58
- button.type = 'button'
59
- button.title = btn.title
60
- 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;'
61
- button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 16 16" fill="currentColor">${btn.icon}</svg>`
62
- button.addEventListener('mouseenter', () => { button.style.opacity = '1'; button.style.background = 'rgba(128,128,128,0.2)' })
63
- button.addEventListener('mouseleave', () => { button.style.opacity = '0.6'; button.style.background = 'none' })
64
- button.addEventListener('mousedown', (e) => {
65
- e.preventDefault()
66
- })
67
- button.addEventListener('click', (e) => {
68
- e.preventDefault()
69
- btn.action()
70
- })
71
- toolbar.appendChild(button)
72
- })
53
+ for (const tool of this.tools) {
54
+ if (typeof tool.toolbarButtons === 'function') {
55
+ tool.toolbarButtons().forEach(btn => this.createToolbarButton(toolbar, btn))
56
+ }
57
+ }
73
58
 
74
59
  // Spacer to push wrap toggle to the right
75
60
  const spacer = document.createElement('div')
@@ -79,12 +64,14 @@ export class MdEditor {
79
64
  // Wrap toggle button
80
65
  this.wrapStorageKey = 'mdEditor_wrap_' + (this.element.id || this.element.name || 'default')
81
66
  const savedWrap = localStorage.getItem(this.wrapStorageKey)
82
- this.wrapEnabled = savedWrap !== null ? savedWrap === 'true' : true
67
+ this.wrapEnabled = savedWrap !== null ? savedWrap === 'true' : this.props.wordWrap
83
68
  this.wrapButton = document.createElement('button')
84
69
  this.wrapButton.type = 'button'
85
70
  this.wrapButton.title = 'Toggle word wrap'
86
71
  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;'
87
- 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>`
72
+ this.wrapButton.style.width = '32px'
73
+ this.wrapButton.style.height = '28px'
74
+ this.loadIcon('text-wrap.svg').then(svg => { this.wrapButton.innerHTML = svg })
88
75
  this.wrapButton.addEventListener('mouseenter', () => { this.wrapButton.style.opacity = '1'; this.wrapButton.style.background = 'rgba(128,128,128,0.2)' })
89
76
  this.wrapButton.addEventListener('mouseleave', () => { this.wrapButton.style.opacity = this.wrapEnabled ? '0.9' : '0.4'; this.wrapButton.style.background = 'none' })
90
77
  this.wrapButton.addEventListener('mousedown', (e) => e.preventDefault())
@@ -101,8 +88,50 @@ export class MdEditor {
101
88
  }
102
89
  }
103
90
 
91
+ createToolbarButton(toolbar, btn) {
92
+ if (btn.separator) {
93
+ const sep = document.createElement('div')
94
+ sep.style.cssText = 'width:1px;align-self:stretch;margin:3px 5px;background:rgba(128,128,128,0.4);'
95
+ toolbar.appendChild(sep)
96
+ return
97
+ }
98
+ const button = document.createElement('button')
99
+ button.type = 'button'
100
+ button.title = btn.title
101
+ 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;'
102
+ if (btn.icon) {
103
+ button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 16 16" fill="currentColor">${btn.icon}</svg>`
104
+ } else if (btn.iconUrl) {
105
+ button.style.width = '32px'
106
+ button.style.height = '28px'
107
+ this.loadIconFromUrl(btn.iconUrl).then(svg => { button.innerHTML = svg })
108
+ } else if (btn.iconFile) {
109
+ button.style.width = '32px'
110
+ button.style.height = '28px'
111
+ this.loadIcon(btn.iconFile).then(svg => { button.innerHTML = svg })
112
+ }
113
+ button.addEventListener('mouseenter', () => { button.style.opacity = '1'; button.style.background = 'rgba(128,128,128,0.2)' })
114
+ button.addEventListener('mouseleave', () => { button.style.opacity = '0.6'; button.style.background = 'none' })
115
+ button.addEventListener('mousedown', (e) => e.preventDefault())
116
+ button.addEventListener('click', (e) => {
117
+ e.preventDefault()
118
+ btn.action()
119
+ })
120
+ toolbar.appendChild(button)
121
+ }
122
+
123
+ loadIcon(filename) {
124
+ const baseUrl = new URL('./tools/icons/', import.meta.url)
125
+ return this.loadIconFromUrl(new URL(filename, baseUrl).href)
126
+ }
127
+
128
+ loadIconFromUrl(url) {
129
+ return fetch(url)
130
+ .then(r => r.text())
131
+ .then(svg => svg.replace(/width="16"/, 'width="20"').replace(/height="16"/, 'height="20"'))
132
+ }
133
+
104
134
  createHighlightBackdrop() {
105
- if (!this.props.syntaxHighlight) return
106
135
  const container = this.element.parentNode
107
136
  container.style.position = 'relative'
108
137
  this.backdrop = document.createElement('div')
@@ -112,7 +141,7 @@ export class MdEditor {
112
141
 
113
142
  // Copy textarea computed styles to backdrop
114
143
  const cs = window.getComputedStyle(this.element)
115
- this.backdrop.style.cssText = `position:absolute;overflow:hidden;pointer-events:none;z-index:1;opacity:${this.props.syntaxHighlight};`
144
+ this.backdrop.style.cssText = `position:absolute;overflow:hidden;pointer-events:none;z-index:1;`
116
145
  this.highlightLayer.style.cssText = `white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;`
117
146
 
118
147
  const syncStyles = () => {
@@ -339,6 +368,13 @@ export class MdEditor {
339
368
  result = result.replace(/(&lt;)(\/?[a-zA-Z]\w*)(.*?)(&gt;)/g, (_, p1, p2, p3, p4) =>
340
369
  this.colorSpan('colorHtmlTag', p1 + p2 + p3 + p4))
341
370
 
371
+ // Tool inline highlighting
372
+ for (const tool of this.tools) {
373
+ if (typeof tool.highlightInline === 'function') {
374
+ result = tool.highlightInline(result)
375
+ }
376
+ }
377
+
342
378
  return result
343
379
  }
344
380
 
@@ -373,21 +409,6 @@ export class MdEditor {
373
409
  this.element.selectionEnd = lineEnd
374
410
  }
375
411
 
376
- toggleHeading(level) {
377
- this.element.focus()
378
- const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
379
- const prefix = '#'.repeat(level) + ' '
380
- const headingMatch = line.match(/^(#{1,6}) /)
381
- this.selectLineRange(lineStart, lineEnd)
382
- if (headingMatch && headingMatch[1].length === level) {
383
- this.insertTextAtCursor(line.substring(prefix.length))
384
- } else if (headingMatch) {
385
- this.insertTextAtCursor(prefix + line.substring(headingMatch[0].length))
386
- } else {
387
- this.insertTextAtCursor(prefix + line)
388
- }
389
- }
390
-
391
412
  toggleWrap(marker) {
392
413
  const start = this.element.selectionStart
393
414
  const end = this.element.selectionEnd
@@ -414,73 +435,13 @@ export class MdEditor {
414
435
  }
415
436
  }
416
437
 
417
- toggleBold() {
418
- this.toggleWrap('**')
419
- }
420
-
421
- toggleItalic() {
422
- this.toggleWrap('_')
423
- }
424
-
425
- insertUnorderedList() {
426
- const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
427
- this.selectLineRange(lineStart, lineEnd)
428
- if (line.startsWith('- ')) {
429
- this.insertTextAtCursor(line.substring(2))
430
- } else {
431
- this.insertTextAtCursor('- ' + line)
432
- }
433
- }
434
-
435
- insertOrderedList() {
436
- const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
437
- this.selectLineRange(lineStart, lineEnd)
438
- const olMatch = line.match(/^\d+\. /)
439
- if (olMatch) {
440
- this.insertTextAtCursor(line.substring(olMatch[0].length))
441
- } else {
442
- this.insertTextAtCursor('1. ' + line)
443
- }
444
- }
445
-
446
- insertLink() {
447
- const start = this.element.selectionStart
448
- const end = this.element.selectionEnd
449
- const selected = this.element.value.substring(start, end)
450
- const url = prompt('Enter URL:')
451
- if (url === null) return
452
- const linkText = selected || 'link'
453
- this.element.focus()
454
- this.selectLineRange(start, end)
455
- this.insertTextAtCursor('[' + linkText + '](' + url + ')')
456
- this.element.selectionStart = start + 1
457
- this.element.selectionEnd = start + 1 + linkText.length
458
- }
459
-
460
- insertImage() {
461
- const start = this.element.selectionStart
462
- const end = this.element.selectionEnd
463
- const selected = this.element.value.substring(start, end)
464
- const url = prompt('Enter image URL:')
465
- if (url === null) return
466
- const altText = selected || 'image'
467
- this.element.focus()
468
- this.selectLineRange(start, end)
469
- this.insertTextAtCursor('![' + altText + '](' + url + ')')
470
- this.element.selectionStart = start + 2
471
- this.element.selectionEnd = start + 2 + altText.length
472
- }
473
-
474
438
  insertTextAtCursor(text) {
475
439
  // execCommand is deprecated, but without alternative to insert text and preserve the correct undo/redo stack
476
440
  document.execCommand("insertText", false, text)
477
441
  }
478
442
 
479
443
  handleKeyDown(e) {
480
- const start = this.element.selectionStart
481
- const end = this.element.selectionEnd
482
- const before = this.element.value.substring(0, start)
483
- const selected = this.element.value.substring(start, end)
444
+ const before = this.element.value.substring(0, this.element.selectionStart)
484
445
  const currentLine = before.substring(before.lastIndexOf('\n') + 1)
485
446
  const isListMode = currentLine.match(/^\t*- /) || currentLine.match(/^\t*\d+\. /)
486
447
  if (e.key === 'Tab') {
@@ -497,17 +458,16 @@ export class MdEditor {
497
458
  } else if (e.key === 'Enter') {
498
459
  this.handleEnterKey(e)
499
460
  } else if (e.ctrlKey || e.metaKey) {
500
- if (e.key === 'b') { // bold
501
- e.preventDefault()
502
- this.toggleBold()
503
- } else if (e.key === 'i') { // italic
504
- e.preventDefault()
505
- this.toggleItalic()
506
- } else if (e.key === 'e') { // game todo this could be an extension
507
- e.preventDefault()
508
- this.insertTextAtCursor('[game id="' + selected + '"]')
509
- this.element.selectionStart = start + 10
510
- this.element.selectionEnd = start + 10 + selected.length
461
+ for (const tool of this.tools) {
462
+ if (typeof tool.keyboardShortcuts === 'function') {
463
+ for (const shortcut of tool.keyboardShortcuts()) {
464
+ if (shortcut.ctrlOrMeta && shortcut.key === e.key) {
465
+ e.preventDefault()
466
+ shortcut.action(e)
467
+ return
468
+ }
469
+ }
470
+ }
511
471
  }
512
472
  }
513
473
  }
@@ -0,0 +1,37 @@
1
+ body {
2
+ margin: 0;
3
+ padding: 1rem;
4
+ background-color: #f8f9fa;
5
+ color: #212529;
6
+ font-family: system-ui, -apple-system, sans-serif;
7
+ }
8
+
9
+ *, *::before, *::after {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ textarea {
14
+ width: 100%;
15
+ height: calc(100vh - 4rem);
16
+ padding: 0.7rem;
17
+ font-family: monospace;
18
+ font-size: 1rem;
19
+ tab-size: 4;
20
+ line-height: 1.3;
21
+ background-color: #fff;
22
+ color: #212529;
23
+ border: 1px solid rgba(128, 128, 128, 0.3);
24
+ border-radius: 4px;
25
+ resize: vertical;
26
+ }
27
+
28
+ @media (prefers-color-scheme: dark) {
29
+ body {
30
+ background-color: #212529;
31
+ color: #dee2e6;
32
+ }
33
+ textarea {
34
+ background-color: #111;
35
+ color: #dee2e6;
36
+ }
37
+ }
@@ -0,0 +1,11 @@
1
+ export class Bold {
2
+ constructor(editor) {
3
+ this.editor = editor
4
+ }
5
+ toolbarButtons() {
6
+ return [{name: 'bold', title: 'Bold', iconFile: 'type-bold.svg', action: () => this.editor.toggleWrap('**')}]
7
+ }
8
+ keyboardShortcuts() {
9
+ return [{key: 'b', ctrlOrMeta: true, action: () => this.editor.toggleWrap('**')}]
10
+ }
11
+ }
@@ -0,0 +1,14 @@
1
+ import {Headings} from "./Headings.js"
2
+ import {Bold} from "./Bold.js"
3
+ import {Italic} from "./Italic.js"
4
+ import {Strikethrough} from "./Strikethrough.js"
5
+ import {UnorderedList} from "./UnorderedList.js"
6
+ import {OrderedList} from "./OrderedList.js"
7
+ import {InsertLink} from "./InsertLink.js"
8
+ import {InsertImage} from "./InsertImage.js"
9
+ import {Separator} from "./Separator.js"
10
+
11
+ export const defaultTools = [Headings, Separator,
12
+ Bold, Italic, Strikethrough, Separator,
13
+ UnorderedList, OrderedList, Separator,
14
+ InsertLink, InsertImage]
@@ -0,0 +1,34 @@
1
+ export class Headings {
2
+ constructor(editor, props = {}) {
3
+ this.editor = editor
4
+ this.minLevel = props.minLevel || 1
5
+ this.maxLevel = props.maxLevel || 3
6
+ }
7
+ toolbarButtons() {
8
+ const buttons = []
9
+ for (let level = this.minLevel; level <= this.maxLevel; level++) {
10
+ buttons.push({
11
+ name: 'h' + level,
12
+ title: 'Heading ' + level,
13
+ iconFile: 'type-h' + level + '.svg',
14
+ action: () => this.toggleHeading(level)
15
+ })
16
+ }
17
+ return buttons
18
+ }
19
+ toggleHeading(level) {
20
+ const editor = this.editor
21
+ editor.element.focus()
22
+ const {lineStart, lineEnd, line} = editor.getCurrentLineInfo()
23
+ const prefix = '#'.repeat(level) + ' '
24
+ const headingMatch = line.match(/^(#{1,6}) /)
25
+ editor.selectLineRange(lineStart, lineEnd)
26
+ if (headingMatch && headingMatch[1].length === level) {
27
+ editor.insertTextAtCursor(line.substring(prefix.length))
28
+ } else if (headingMatch) {
29
+ editor.insertTextAtCursor(prefix + line.substring(headingMatch[0].length))
30
+ } else {
31
+ editor.insertTextAtCursor(prefix + line)
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,22 @@
1
+ export class InsertImage {
2
+ constructor(editor) {
3
+ this.editor = editor
4
+ }
5
+ toolbarButtons() {
6
+ return [{name: 'image', title: 'Insert Image', iconFile: 'card-image.svg', action: () => this.insertImage()}]
7
+ }
8
+ insertImage() {
9
+ const el = this.editor.element
10
+ const start = el.selectionStart
11
+ const end = el.selectionEnd
12
+ const selected = el.value.substring(start, end)
13
+ const url = prompt('Enter image URL:')
14
+ if (url === null) return
15
+ const altText = selected || 'image'
16
+ el.focus()
17
+ this.editor.selectLineRange(start, end)
18
+ this.editor.insertTextAtCursor('![' + altText + '](' + url + ')')
19
+ el.selectionStart = start + 2
20
+ el.selectionEnd = start + 2 + altText.length
21
+ }
22
+ }
@@ -0,0 +1,22 @@
1
+ export class InsertLink {
2
+ constructor(editor) {
3
+ this.editor = editor
4
+ }
5
+ toolbarButtons() {
6
+ return [{name: 'link', title: 'Insert Link', iconFile: 'link-45deg.svg', action: () => this.insertLink()}]
7
+ }
8
+ insertLink() {
9
+ const el = this.editor.element
10
+ const start = el.selectionStart
11
+ const end = el.selectionEnd
12
+ const selected = el.value.substring(start, end)
13
+ const url = prompt('Enter URL:')
14
+ if (url === null) return
15
+ const linkText = selected || 'link'
16
+ el.focus()
17
+ this.editor.selectLineRange(start, end)
18
+ this.editor.insertTextAtCursor('[' + linkText + '](' + url + ')')
19
+ el.selectionStart = start + 1
20
+ el.selectionEnd = start + 1 + linkText.length
21
+ }
22
+ }
@@ -0,0 +1,11 @@
1
+ export class Italic {
2
+ constructor(editor) {
3
+ this.editor = editor
4
+ }
5
+ toolbarButtons() {
6
+ return [{name: 'italic', title: 'Italic', iconFile: 'type-italic.svg', action: () => this.editor.toggleWrap('_')}]
7
+ }
8
+ keyboardShortcuts() {
9
+ return [{key: 'i', ctrlOrMeta: true, action: () => this.editor.toggleWrap('_')}]
10
+ }
11
+ }
@@ -0,0 +1,19 @@
1
+ export class OrderedList {
2
+ constructor(editor) {
3
+ this.editor = editor
4
+ }
5
+ toolbarButtons() {
6
+ return [{name: 'ol', title: 'Ordered List', iconFile: 'list-ol.svg', action: () => this.insertOrderedList()}]
7
+ }
8
+ insertOrderedList() {
9
+ const editor = this.editor
10
+ const {lineStart, lineEnd, line} = editor.getCurrentLineInfo()
11
+ editor.selectLineRange(lineStart, lineEnd)
12
+ const olMatch = line.match(/^\d+\. /)
13
+ if (olMatch) {
14
+ editor.insertTextAtCursor(line.substring(olMatch[0].length))
15
+ } else {
16
+ editor.insertTextAtCursor('1. ' + line)
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ export class Separator {
2
+ constructor(editor) {
3
+ this.editor = editor
4
+ }
5
+ toolbarButtons() {
6
+ return [{name: 'separator', title: '', separator: true}]
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ export class Strikethrough {
2
+ constructor(editor) {
3
+ this.editor = editor
4
+ }
5
+ toolbarButtons() {
6
+ return [{name: 'strikethrough', title: 'Strikethrough', iconFile: 'type-strikethrough.svg', action: () => this.editor.toggleWrap('~~')}]
7
+ }
8
+ }
@@ -0,0 +1,18 @@
1
+ export class UnorderedList {
2
+ constructor(editor) {
3
+ this.editor = editor
4
+ }
5
+ toolbarButtons() {
6
+ return [{name: 'ul', title: 'Unordered List', iconFile: 'list-ul.svg', action: () => this.insertUnorderedList()}]
7
+ }
8
+ insertUnorderedList() {
9
+ const editor = this.editor
10
+ const {lineStart, lineEnd, line} = editor.getCurrentLineInfo()
11
+ editor.selectLineRange(lineStart, lineEnd)
12
+ if (line.startsWith('- ')) {
13
+ editor.insertTextAtCursor(line.substring(2))
14
+ } else {
15
+ editor.insertTextAtCursor('- ' + line)
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019-2024 The Bootstrap Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-card-image" viewBox="0 0 16 16">
2
+ <path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
3
+ <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"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-link-45deg" viewBox="0 0 16 16">
2
+ <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"/>
3
+ <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"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-ol" viewBox="0 0 16 16">
2
+ <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"/>
3
+ <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"/>
4
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-ul" viewBox="0 0 16 16">
2
+ <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"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-wrap" viewBox="0 0 16 16">
2
+ <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"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-bold" viewBox="0 0 16 16">
2
+ <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"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h1" viewBox="0 0 16 16">
2
+ <path d="M7.648 13V3H6.3v4.234H1.348V3H0v10h1.348V8.421H6.3V13zM14 13V3h-1.333l-2.381 1.766V6.12L12.6 4.443h.066V13z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h2" viewBox="0 0 16 16">
2
+ <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"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h3" viewBox="0 0 16 16">
2
+ <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"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-italic" viewBox="0 0 16 16">
2
+ <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"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-strikethrough" viewBox="0 0 16 16">
2
+ <path d="M6.333 5.686c0 .31.083.581.27.814H5.166a2.8 2.8 0 0 1-.099-.76c0-1.627 1.436-2.768 3.48-2.768 1.969 0 3.39 1.175 3.445 2.85h-1.23c-.11-1.08-.964-1.743-2.25-1.743-1.23 0-2.18.602-2.18 1.607zm2.194 7.478c-2.153 0-3.589-1.107-3.705-2.81h1.23c.144 1.06 1.129 1.703 2.544 1.703 1.34 0 2.31-.705 2.31-1.675 0-.827-.547-1.374-1.914-1.675L8.046 8.5H1v-1h14v1h-3.504c.468.437.675.994.675 1.697 0 1.826-1.436 2.967-3.644 2.967"/>
3
+ </svg>