cm-md-editor 2.4.0 → 3.0.1

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,13 @@
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
+ "Bash(npm whoami:*)"
11
+ ]
12
+ }
13
+ }
package/README.md CHANGED
@@ -9,11 +9,11 @@ A minimal, dependency-free markdown editor as a vanilla JavaScript ES6 module.
9
9
  ## Key features
10
10
 
11
11
  - Vanilla JavaScript module, zero dependencies
12
- - Syntax highlighting for headings, bold, italic, code, lists, links, images, blockquotes, HTML tags, horizontal rules, front matter and more
13
- - Toolbar with configurable buttons (headings, bold, italic, lists, links, images)
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
14
  - Word wrap toggle with persistent state (localStorage)
15
15
  - List mode: Tab/Shift-Tab to indent/outdent, auto-continuation on Enter
16
- - Bold with Ctrl/Cmd+B, italic with Ctrl/Cmd+I
16
+ - Bold with Ctrl/Cmd+B, italic with Ctrl/Cmd+I (provided by tools)
17
17
  - Native undo/redo support (Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z)
18
18
  - Lightweight, fast, easy to use
19
19
 
@@ -35,12 +35,32 @@ npm install cm-md-editor
35
35
  </script>
36
36
  ```
37
37
 
38
- With custom configuration:
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:
39
60
 
40
61
  ```javascript
41
- const editor = new MdEditor(document.getElementById("editor"), {
42
- wordWrap: false,
43
- toolbarButtons: ["h1", "h2", "bold", "italic", "ul", "link"]
62
+ new MdEditor(document.getElementById("editor"), {
63
+ tools: [[Headings, {minLevel: 2, maxLevel: 4}], Bold, Italic]
44
64
  })
45
65
  ```
46
66
 
@@ -50,8 +70,8 @@ All props are optional. Pass them as the second argument to the constructor.
50
70
 
51
71
  | Prop | Type | Default | Description |
52
72
  |------|------|---------|-------------|
73
+ | `tools` | `array` | `defaultTools` | Array of tool classes (or `[class, props]` tuples). See [Tools](#tools) |
53
74
  | `wordWrap` | `boolean` | `true` | Default word wrap state. Overridden by localStorage if the user has toggled it |
54
- | `toolbarButtons` | `string[]` | `["h1", "h2", "h3", "bold", "italic", "ul", "ol", "link", "image"]` | Which toolbar buttons to show. Available: `h1`, `h2`, `h3`, `bold`, `italic`, `ul`, `ol`, `link`, `image` |
55
75
  | `colorHeading` | `string` | `"100,160,255"` | RGB color for headings |
56
76
  | `colorCode` | `string` | `"130,170,200"` | RGB color for code spans and fenced code blocks |
57
77
  | `colorComment` | `string` | `"128,128,128"` | RGB color for HTML comments |
@@ -68,15 +88,126 @@ All props are optional. Pass them as the second argument to the constructor.
68
88
 
69
89
  Colors are specified as RGB strings (e.g. `"255,180,80"`) and rendered at full opacity.
70
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
+
71
202
  ## Keyboard shortcuts
72
203
 
73
- | Shortcut | Action |
74
- |----------|--------|
75
- | Ctrl/Cmd + B | Toggle bold |
76
- | Ctrl/Cmd + I | Toggle italic |
77
- | Tab | Indent list item or insert tab |
78
- | Shift + Tab | Outdent list item |
79
- | Enter | Auto-continue list (unordered and ordered) |
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 |
80
211
 
81
212
  ## License
82
213
 
@@ -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.4.0",
3
+ "version": "3.0.1",
4
4
  "description": "a simple markdown editor",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/MdEditor.js CHANGED
@@ -2,6 +2,8 @@
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) {
@@ -21,9 +23,17 @@ export class MdEditor {
21
23
  colorEscape: "128,128,128",
22
24
  colorFrontMatter: "128,128,200",
23
25
  wordWrap: true,
24
- toolbarButtons: ["h1", "h2", "h3", "bold", "italic", "ul", "ol", "link", "image"],
26
+ iconsPath: new URL('./tools/icons/', import.meta.url).href,
27
+ tools: defaultTools,
25
28
  ...props
26
29
  }
30
+ this.tools = this.props.tools.map(entry => {
31
+ if (Array.isArray(entry)) {
32
+ const [Tool, toolProps] = entry
33
+ return new Tool(this, toolProps)
34
+ }
35
+ return new entry(this)
36
+ })
27
37
  this.element.addEventListener('keydown', (e) => this.handleKeyDown(e))
28
38
  this.createToolbar()
29
39
  this.createHighlightBackdrop()
@@ -41,35 +51,11 @@ export class MdEditor {
41
51
  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
52
  wrapper.insertBefore(toolbar, this.element)
43
53
  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
- })
54
+ for (const tool of this.tools) {
55
+ if (typeof tool.toolbarButtons === 'function') {
56
+ tool.toolbarButtons().forEach(btn => this.createToolbarButton(toolbar, btn))
57
+ }
58
+ }
73
59
 
74
60
  // Spacer to push wrap toggle to the right
75
61
  const spacer = document.createElement('div')
@@ -84,7 +70,9 @@ export class MdEditor {
84
70
  this.wrapButton.type = 'button'
85
71
  this.wrapButton.title = 'Toggle word wrap'
86
72
  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>`
73
+ this.wrapButton.style.width = '32px'
74
+ this.wrapButton.style.height = '28px'
75
+ this.loadIcon('text-wrap.svg').then(svg => { this.wrapButton.innerHTML = svg })
88
76
  this.wrapButton.addEventListener('mouseenter', () => { this.wrapButton.style.opacity = '1'; this.wrapButton.style.background = 'rgba(128,128,128,0.2)' })
89
77
  this.wrapButton.addEventListener('mouseleave', () => { this.wrapButton.style.opacity = this.wrapEnabled ? '0.9' : '0.4'; this.wrapButton.style.background = 'none' })
90
78
  this.wrapButton.addEventListener('mousedown', (e) => e.preventDefault())
@@ -101,6 +89,48 @@ export class MdEditor {
101
89
  }
102
90
  }
103
91
 
92
+ createToolbarButton(toolbar, btn) {
93
+ if (btn.separator) {
94
+ const sep = document.createElement('div')
95
+ sep.style.cssText = 'width:1px;align-self:stretch;margin:3px 5px;background:rgba(128,128,128,0.4);'
96
+ toolbar.appendChild(sep)
97
+ return
98
+ }
99
+ const button = document.createElement('button')
100
+ button.type = 'button'
101
+ button.title = btn.title
102
+ 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;'
103
+ if (btn.icon) {
104
+ button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 16 16" fill="currentColor">${btn.icon}</svg>`
105
+ } else if (btn.iconUrl) {
106
+ button.style.width = '32px'
107
+ button.style.height = '28px'
108
+ this.loadIconFromUrl(btn.iconUrl).then(svg => { button.innerHTML = svg })
109
+ } else if (btn.iconFile) {
110
+ button.style.width = '32px'
111
+ button.style.height = '28px'
112
+ this.loadIcon(btn.iconFile).then(svg => { button.innerHTML = svg })
113
+ }
114
+ button.addEventListener('mouseenter', () => { button.style.opacity = '1'; button.style.background = 'rgba(128,128,128,0.2)' })
115
+ button.addEventListener('mouseleave', () => { button.style.opacity = '0.6'; button.style.background = 'none' })
116
+ button.addEventListener('mousedown', (e) => e.preventDefault())
117
+ button.addEventListener('click', (e) => {
118
+ e.preventDefault()
119
+ btn.action()
120
+ })
121
+ toolbar.appendChild(button)
122
+ }
123
+
124
+ loadIcon(filename) {
125
+ return this.loadIconFromUrl(new URL(filename, this.props.iconsPath).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
135
  const container = this.element.parentNode
106
136
  container.style.position = 'relative'
@@ -338,6 +368,13 @@ export class MdEditor {
338
368
  result = result.replace(/(&lt;)(\/?[a-zA-Z]\w*)(.*?)(&gt;)/g, (_, p1, p2, p3, p4) =>
339
369
  this.colorSpan('colorHtmlTag', p1 + p2 + p3 + p4))
340
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
+
341
378
  return result
342
379
  }
343
380
 
@@ -372,21 +409,6 @@ export class MdEditor {
372
409
  this.element.selectionEnd = lineEnd
373
410
  }
374
411
 
375
- toggleHeading(level) {
376
- this.element.focus()
377
- const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
378
- const prefix = '#'.repeat(level) + ' '
379
- const headingMatch = line.match(/^(#{1,6}) /)
380
- this.selectLineRange(lineStart, lineEnd)
381
- if (headingMatch && headingMatch[1].length === level) {
382
- this.insertTextAtCursor(line.substring(prefix.length))
383
- } else if (headingMatch) {
384
- this.insertTextAtCursor(prefix + line.substring(headingMatch[0].length))
385
- } else {
386
- this.insertTextAtCursor(prefix + line)
387
- }
388
- }
389
-
390
412
  toggleWrap(marker) {
391
413
  const start = this.element.selectionStart
392
414
  const end = this.element.selectionEnd
@@ -413,73 +435,13 @@ export class MdEditor {
413
435
  }
414
436
  }
415
437
 
416
- toggleBold() {
417
- this.toggleWrap('**')
418
- }
419
-
420
- toggleItalic() {
421
- this.toggleWrap('_')
422
- }
423
-
424
- insertUnorderedList() {
425
- const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
426
- this.selectLineRange(lineStart, lineEnd)
427
- if (line.startsWith('- ')) {
428
- this.insertTextAtCursor(line.substring(2))
429
- } else {
430
- this.insertTextAtCursor('- ' + line)
431
- }
432
- }
433
-
434
- insertOrderedList() {
435
- const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
436
- this.selectLineRange(lineStart, lineEnd)
437
- const olMatch = line.match(/^\d+\. /)
438
- if (olMatch) {
439
- this.insertTextAtCursor(line.substring(olMatch[0].length))
440
- } else {
441
- this.insertTextAtCursor('1. ' + line)
442
- }
443
- }
444
-
445
- insertLink() {
446
- const start = this.element.selectionStart
447
- const end = this.element.selectionEnd
448
- const selected = this.element.value.substring(start, end)
449
- const url = prompt('Enter URL:')
450
- if (url === null) return
451
- const linkText = selected || 'link'
452
- this.element.focus()
453
- this.selectLineRange(start, end)
454
- this.insertTextAtCursor('[' + linkText + '](' + url + ')')
455
- this.element.selectionStart = start + 1
456
- this.element.selectionEnd = start + 1 + linkText.length
457
- }
458
-
459
- insertImage() {
460
- const start = this.element.selectionStart
461
- const end = this.element.selectionEnd
462
- const selected = this.element.value.substring(start, end)
463
- const url = prompt('Enter image URL:')
464
- if (url === null) return
465
- const altText = selected || 'image'
466
- this.element.focus()
467
- this.selectLineRange(start, end)
468
- this.insertTextAtCursor('![' + altText + '](' + url + ')')
469
- this.element.selectionStart = start + 2
470
- this.element.selectionEnd = start + 2 + altText.length
471
- }
472
-
473
438
  insertTextAtCursor(text) {
474
439
  // execCommand is deprecated, but without alternative to insert text and preserve the correct undo/redo stack
475
440
  document.execCommand("insertText", false, text)
476
441
  }
477
442
 
478
443
  handleKeyDown(e) {
479
- const start = this.element.selectionStart
480
- const end = this.element.selectionEnd
481
- const before = this.element.value.substring(0, start)
482
- const selected = this.element.value.substring(start, end)
444
+ const before = this.element.value.substring(0, this.element.selectionStart)
483
445
  const currentLine = before.substring(before.lastIndexOf('\n') + 1)
484
446
  const isListMode = currentLine.match(/^\t*- /) || currentLine.match(/^\t*\d+\. /)
485
447
  if (e.key === 'Tab') {
@@ -496,17 +458,16 @@ export class MdEditor {
496
458
  } else if (e.key === 'Enter') {
497
459
  this.handleEnterKey(e)
498
460
  } else if (e.ctrlKey || e.metaKey) {
499
- if (e.key === 'b') { // bold
500
- e.preventDefault()
501
- this.toggleBold()
502
- } else if (e.key === 'i') { // italic
503
- e.preventDefault()
504
- this.toggleItalic()
505
- } else if (e.key === 'e') { // game todo this could be an extension
506
- e.preventDefault()
507
- this.insertTextAtCursor('[game id="' + selected + '"]')
508
- this.element.selectionStart = start + 10
509
- 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
+ }
510
471
  }
511
472
  }
512
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>