smart-md-editor 0.5.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.
- package/LICENSE +21 -0
- package/README.md +974 -0
- package/dist/smart-editor.cjs.js +83280 -0
- package/dist/smart-editor.cjs.js.map +1 -0
- package/dist/smart-editor.esm.js +83272 -0
- package/dist/smart-editor.esm.js.map +1 -0
- package/dist/smart-editor.iife.js +83285 -0
- package/dist/smart-editor.iife.js.map +1 -0
- package/package.json +61 -0
- package/scripts/download-drawio.mjs +220 -0
package/README.md
ADDED
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
# Smart Editor
|
|
2
|
+
|
|
3
|
+
[](https://github.com/florerion/SmartEditor/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/florerion/SmartEditor/actions/workflows/coverage.yml)
|
|
5
|
+
[](https://github.com/florerion/SmartEditor/releases)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
A framework-agnostic Markdown editor for web apps with split code/preview UX, runtime API, extensible toolbar actions, markdown-it parsing, and source-line synchronization.
|
|
9
|
+
|
|
10
|
+
This document is for developers integrating the editor into their own application. It is not an end-user guide for writing Markdown.
|
|
11
|
+
|
|
12
|
+
## Contents
|
|
13
|
+
|
|
14
|
+
- Getting Started
|
|
15
|
+
- Supported Code Block Languages
|
|
16
|
+
- Embedding the Editor on a Page
|
|
17
|
+
- Configuration Options
|
|
18
|
+
- Runtime API
|
|
19
|
+
- Events and Callback Usage
|
|
20
|
+
- Built-in Plugins and Features
|
|
21
|
+
- Extending Functionality (Custom Toolbar Buttons)
|
|
22
|
+
- Versioned API Changes
|
|
23
|
+
- Markdown Compatibility Notes
|
|
24
|
+
- Security Notes
|
|
25
|
+
- License
|
|
26
|
+
- Development Commands
|
|
27
|
+
- Troubleshooting
|
|
28
|
+
|
|
29
|
+
## Getting Started
|
|
30
|
+
|
|
31
|
+
### Requirements
|
|
32
|
+
|
|
33
|
+
- Modern browser with ES module support.
|
|
34
|
+
- A container element with explicit height (important for CodeMirror layout).
|
|
35
|
+
- Optional globals for enhanced preview:
|
|
36
|
+
- `window.mermaid` for Mermaid rendering.
|
|
37
|
+
- KaTeX CSS for math rendering visuals.
|
|
38
|
+
- Fenced code blocks with an explicit language, for example `javascript`, are syntax-highlighted in preview.
|
|
39
|
+
|
|
40
|
+
### Install dependencies (project development)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Build
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm run build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Build output is written to `dist/`:
|
|
53
|
+
|
|
54
|
+
- `dist/smart-editor.esm.js`
|
|
55
|
+
- `dist/smart-editor.cjs.js`
|
|
56
|
+
- `dist/smart-editor.iife.js`
|
|
57
|
+
|
|
58
|
+
## Supported Code Block Languages
|
|
59
|
+
|
|
60
|
+
Syntax highlighting in preview is enabled for fenced code blocks with explicit language names, for example:
|
|
61
|
+
|
|
62
|
+
````markdown
|
|
63
|
+
```javascript
|
|
64
|
+
console.log('hello');
|
|
65
|
+
```
|
|
66
|
+
````
|
|
67
|
+
|
|
68
|
+
Supported language labels:
|
|
69
|
+
|
|
70
|
+
- `bash`
|
|
71
|
+
- `c`
|
|
72
|
+
- `cpp`
|
|
73
|
+
- `diff`
|
|
74
|
+
- `django`
|
|
75
|
+
- `dockerfile`
|
|
76
|
+
- `excel`
|
|
77
|
+
- `graphql`
|
|
78
|
+
- `handlebars`
|
|
79
|
+
- `http`
|
|
80
|
+
- `java`
|
|
81
|
+
- `javascript`
|
|
82
|
+
- `json`
|
|
83
|
+
- `kotlin`
|
|
84
|
+
- `lisp`
|
|
85
|
+
- `lua`
|
|
86
|
+
- `makefile`
|
|
87
|
+
- `markdown`
|
|
88
|
+
- `mathematica`
|
|
89
|
+
- `matlab`
|
|
90
|
+
- `nginx`
|
|
91
|
+
- `objectivec`
|
|
92
|
+
- `perl`
|
|
93
|
+
- `php`
|
|
94
|
+
- `plaintext`
|
|
95
|
+
- `powershell`
|
|
96
|
+
- `python`
|
|
97
|
+
- `ruby`
|
|
98
|
+
- `sql`
|
|
99
|
+
- `scala`
|
|
100
|
+
- `shell`
|
|
101
|
+
- `swift`
|
|
102
|
+
- `typescript`
|
|
103
|
+
- `xml`
|
|
104
|
+
|
|
105
|
+
Common aliases are accepted (for example `js`, `ts`, `html`, `sh`, `ps1`, `gql`, `md`, `objc`).
|
|
106
|
+
|
|
107
|
+
Special fallback:
|
|
108
|
+
|
|
109
|
+
- `curl` is mapped to `bash` highlighting.
|
|
110
|
+
|
|
111
|
+
## Embedding the Editor on a Page
|
|
112
|
+
|
|
113
|
+
### Option A: Vanilla JS (`createEditor`)
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
<div id="editor" style="height: 600px;"></div>
|
|
117
|
+
|
|
118
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css">
|
|
119
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
120
|
+
<script>
|
|
121
|
+
mermaid.initialize({ startOnLoad: false });
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<script type="module">
|
|
125
|
+
import { createEditor } from './dist/smart-editor.esm.js';
|
|
126
|
+
|
|
127
|
+
const editor = createEditor('#editor', {
|
|
128
|
+
value: '# Hello',
|
|
129
|
+
mode: 'split',
|
|
130
|
+
onChange: (markdown, tokens, html) => {
|
|
131
|
+
console.log(markdown.length, tokens.length);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
window.editor = editor;
|
|
136
|
+
</script>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Option B: Web Component (`<smart-editor>`)
|
|
140
|
+
|
|
141
|
+
Importing the library registers the custom element as a side effect.
|
|
142
|
+
|
|
143
|
+
```html
|
|
144
|
+
<smart-editor id="mde" mode="split" theme="auto" style="height: 600px;"></smart-editor>
|
|
145
|
+
|
|
146
|
+
<script type="module">
|
|
147
|
+
import './dist/smart-editor.esm.js';
|
|
148
|
+
|
|
149
|
+
const el = document.getElementById('mde');
|
|
150
|
+
el.setMarkdown('# Initial content');
|
|
151
|
+
|
|
152
|
+
el.addEventListener('se-change', (e) => {
|
|
153
|
+
console.log(e.detail.markdown);
|
|
154
|
+
});
|
|
155
|
+
</script>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Configuration Options
|
|
159
|
+
|
|
160
|
+
`createEditor(element, options)` and `new EditorCore(element, options)` use the same options schema.
|
|
161
|
+
|
|
162
|
+
| Option | Type | Default | Description |
|
|
163
|
+
|---|---|---|---|
|
|
164
|
+
| `value` | `string` | `''` | Initial markdown content. |
|
|
165
|
+
| `mode` | `'split' \| 'code' \| 'preview' \| 'wysiwyg'` | `'split'` | Initial view mode (`wysiwyg` is preview-first beta mode). |
|
|
166
|
+
| `scrollSync` | `boolean` | `true` | In `split` mode, synchronizes vertical scrolling between code and preview using smooth animated follow. |
|
|
167
|
+
| `theme` | `'auto' \| 'light' \| 'dark' \| 'sepia' \| 'midnight' \| 'solarized' \| 'nord' \| 'high-contrast'` | `'auto'` | Theme id applied to the editor root. `auto` follows the OS color scheme; the built-in presets can also be switched at runtime. |
|
|
168
|
+
| `markdown.options` | `object` | `{}` | Options passed to `markdown-it`. |
|
|
169
|
+
| `markdown.plugins` | `Array` | `[]` | Extra markdown-it plugins: `[[pluginFn, pluginOpts?], ...]`. |
|
|
170
|
+
| `upload.endpoint` | `string` | `undefined` | Default upload endpoint (`POST multipart/form-data`) used for all file types with no matching entry in `upload.endpoints`. Images fall back to base64 when omitted or on error; non-image files require an endpoint and are rejected without one. |
|
|
171
|
+
| `upload.endpoints` | `Object.<string,string>` | `undefined` | Per-type endpoint overrides. Keys can be a MIME type (`image/png`), a wildcard (`image/*`), or a file extension (`.pdf`). The first matching entry wins; unmatched files fall back to `upload.endpoint`. Example: `{ 'image/*': '/upload/image', 'application/pdf': '/upload/raw' }` |
|
|
172
|
+
| `upload.headers` | `object` | `{}` | Extra HTTP headers for upload requests (e.g. `Authorization`). |
|
|
173
|
+
| `upload.extraFields` | `object` | `{}` | Extra FormData fields appended to every upload (e.g. `{ upload_preset: 'my_preset' }` for Cloudinary unsigned upload). |
|
|
174
|
+
| `upload.responseUrlField` | `string` | `'url'` | JSON field in the upload response that holds the asset URL (e.g. `'secure_url'` for Cloudinary). |
|
|
175
|
+
| `upload.maxSize` | `number` | `5 * 1024 * 1024` | Max image size in bytes. |
|
|
176
|
+
| `upload.fileMaxSize` | `number` | `upload.maxSize` | Max non-image file size in bytes. |
|
|
177
|
+
| `upload.formats` | `string[]` | common image MIME list | Allowed image MIME types. |
|
|
178
|
+
| `upload.fileFormats` | `string[]` | `undefined` | Allowed non-image MIME types/extensions (for example `application/pdf`, `.docx`). If omitted, non-image files are accepted. |
|
|
179
|
+
| `upload.pickerAccept` | `string` | `*/*` | Value for file-picker `accept` attribute. |
|
|
180
|
+
| `drawio.url` | `string` | `https://embed.diagrams.net/?embed=1&proto=json&spin=1&ui=min&libraries=1` | draw.io embed URL used by modal. Set your own URL for self-hosted/offline mode. |
|
|
181
|
+
| `drawio.allowHostedFallback` | `boolean` | `true` | When `true`, editor retries with `https://embed.diagrams.net` if local/self-hosted draw.io fails to initialize. Set to `false` for strict offline mode. |
|
|
182
|
+
| `toolbar` | `object` | `undefined` | Declarative toolbar layout: visible items, grouping, ordering, display mode, and dropdown menus. |
|
|
183
|
+
| `busy.showDelay` | `number` | `140` | Delay (ms) before showing loading overlay (anti-flicker for very fast tasks). |
|
|
184
|
+
| `busy.minVisible` | `number` | `180` | Minimum overlay visibility time (ms) once shown, to avoid flashing. |
|
|
185
|
+
| `busy.texts.defaultLabel` | `string` | `'Working...'` | Default busy label used when task does not provide one. |
|
|
186
|
+
| `busy.texts.cancel` | `string` | `'Cancel'` | Cancel button label in the loading overlay. |
|
|
187
|
+
| `compatibility.enabled` | `boolean` | `false` | Enables publishing-compatibility validation and suggested fixes. |
|
|
188
|
+
| `compatibility.showPanel` | `boolean` | `false` (auto `true` when enabled) | Shows built-in compatibility status panel above editor panes. |
|
|
189
|
+
| `compatibility.debounce` | `number` | `500` | Validation debounce in milliseconds while typing. |
|
|
190
|
+
| `compatibility.showPreviewUsingProfile` | `boolean` | `false` | When enabled, preview uses HTML generated by the compatibility profile. |
|
|
191
|
+
| `compatibility.markdownIt` | `object` | Eleventy-like defaults | markdown-it options for the built-in Eleventy compatibility profile. |
|
|
192
|
+
| `compatibility.plugins` | `Array` | `[]` | Extra markdown-it plugins for compatibility profile, e.g. `[[markdownItAnchor, opts]]`. |
|
|
193
|
+
| `compatibility.disableRules` | `string[]` | `['emphasis']` | markdown-it rules disabled by the built-in Eleventy compatibility profile. |
|
|
194
|
+
| `compatibility.profile` | `object` | Eleventy markdown-it profile | Custom profile implementing `render(markdown) => { html, tokens? }`. |
|
|
195
|
+
| `compatibility.rules` | `Array` | built-in table rule | Validation/fix rules used by compatibility service. |
|
|
196
|
+
| `onChange` | `function` | `undefined` | Called with `(markdown, tokens, html)`. |
|
|
197
|
+
| `onSelectionChange` | `function` | `undefined` | Called with current selection object. |
|
|
198
|
+
| `onPaste` | `function` | `undefined` | Native paste event hook. |
|
|
199
|
+
| `onUploadStart` | `function` | `undefined` | Called with `(file)`. |
|
|
200
|
+
| `onUploadDone` | `function` | `undefined` | Called with `(file, urlOrBase64)`. |
|
|
201
|
+
| `onUploadError` | `function` | `undefined` | Called with `(file, error)`. |
|
|
202
|
+
| `onPreviewClick` | `function` | `undefined` | Called with `(element, { from, to })`. |
|
|
203
|
+
| `onCommand` | `function` | `undefined` | Called before `runCommand(id, args)`. |
|
|
204
|
+
| `onCompatibilityReport` | `function` | `undefined` | Called with latest compatibility report object. |
|
|
205
|
+
| `onCompatibilityStatusChange` | `function` | `undefined` | Called with `(status, report)` on status transitions. |
|
|
206
|
+
| `onCompatibilityFixApplied` | `function` | `undefined` | Called after user accepts compatibility fix proposal. |
|
|
207
|
+
| `onBusyChange` | `function` | `undefined` | Called with busy overlay state `{ busy, count, label, detail, scope, locked, canCancel, cancelToken }`. |
|
|
208
|
+
|
|
209
|
+
## Compatibility Quick Start (Eleventy)
|
|
210
|
+
|
|
211
|
+
Use this setup when your production publishing pipeline is Eleventy and you want editor preview/validation to match it as closely as possible.
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
import { createEditor, createEleventyCompatibilityProfile } from 'smart-md-editor';
|
|
215
|
+
import markdownItAnchor from 'markdown-it-anchor';
|
|
216
|
+
import markdownItCollapsible from 'markdown-it-collapsible';
|
|
217
|
+
|
|
218
|
+
const profile = createEleventyCompatibilityProfile({
|
|
219
|
+
markdownIt: {
|
|
220
|
+
html: true,
|
|
221
|
+
breaks: true,
|
|
222
|
+
linkify: true,
|
|
223
|
+
},
|
|
224
|
+
disableRules: ['emphasis'],
|
|
225
|
+
plugins: [
|
|
226
|
+
[markdownItAnchor, {
|
|
227
|
+
permalink: true,
|
|
228
|
+
permalinkSymbol: '',
|
|
229
|
+
permalinkBefore: false,
|
|
230
|
+
}],
|
|
231
|
+
[markdownItCollapsible, {}],
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const editor = createEditor('#editor', {
|
|
236
|
+
compatibility: {
|
|
237
|
+
enabled: true,
|
|
238
|
+
showPanel: true,
|
|
239
|
+
profile,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Notes:
|
|
245
|
+
- Keep Eleventy and editor markdown-it options/plugins aligned.
|
|
246
|
+
- The compatibility panel supports per-issue jump + fix and batch fix flow.
|
|
247
|
+
- Built-in table diagnostics include: `table.missing-leading-pipe`, `table.missing-trailing-pipe`, `table.column-count-mismatch`, `table.invalid-separator-row`.
|
|
248
|
+
|
|
249
|
+
### Example: upload + parser plugins
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
const editor = createEditor('#editor', {
|
|
253
|
+
value: '# Content',
|
|
254
|
+
scrollSync: true,
|
|
255
|
+
busy: {
|
|
256
|
+
showDelay: 160,
|
|
257
|
+
minVisible: 220,
|
|
258
|
+
texts: {
|
|
259
|
+
defaultLabel: 'Przetwarzanie...',
|
|
260
|
+
cancel: 'Anuluj',
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
upload: {
|
|
264
|
+
// Option A: custom backend
|
|
265
|
+
endpoint: '/api/upload',
|
|
266
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
267
|
+
maxSize: 8 * 1024 * 1024,
|
|
268
|
+
formats: ['image/png', 'image/jpeg', 'image/webp'],
|
|
269
|
+
fileFormats: [
|
|
270
|
+
'application/pdf',
|
|
271
|
+
'application/msword',
|
|
272
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
273
|
+
'application/vnd.ms-excel',
|
|
274
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
275
|
+
],
|
|
276
|
+
pickerAccept: 'image/*,.pdf,.doc,.docx,.xls,.xlsx',
|
|
277
|
+
|
|
278
|
+
// Option B: Cloudinary unsigned direct upload (no backend needed)
|
|
279
|
+
// endpoint: 'https://api.cloudinary.com/v1_1/YOUR_CLOUD_NAME/image/upload',
|
|
280
|
+
// extraFields: { upload_preset: 'YOUR_UPLOAD_PRESET' },
|
|
281
|
+
// responseUrlField: 'secure_url',
|
|
282
|
+
// maxSize: 10 * 1024 * 1024,
|
|
283
|
+
},
|
|
284
|
+
markdown: {
|
|
285
|
+
options: { html: true, linkify: true, typographer: true },
|
|
286
|
+
plugins: [
|
|
287
|
+
[someMarkdownItPlugin, { someOption: true }],
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Example: theme selection
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
const editor = createEditor('#editor', {
|
|
297
|
+
theme: 'sepia',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
editor.setTheme('midnight');
|
|
301
|
+
console.log(editor.getAvailableThemes());
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Built-in presets:
|
|
305
|
+
|
|
306
|
+
- `light`: neutral bright UI
|
|
307
|
+
- `dark`: neutral dark UI
|
|
308
|
+
- `sepia`: warm reading theme for long-form writing
|
|
309
|
+
- `midnight`: cool dark coding theme with stronger contrast
|
|
310
|
+
- `solarized`: classic balanced palette inspired by Solarized
|
|
311
|
+
- `nord`: cool arctic dark palette
|
|
312
|
+
- `high-contrast`: accessibility-focused very high contrast variant
|
|
313
|
+
|
|
314
|
+
## Runtime API
|
|
315
|
+
|
|
316
|
+
Returned editor instance (or `<smart-editor>` proxies) provides:
|
|
317
|
+
|
|
318
|
+
| Method | Signature | Description |
|
|
319
|
+
|---|---|---|
|
|
320
|
+
| `getMarkdown` | `() => string` | Get current markdown string. |
|
|
321
|
+
| `setMarkdown` | `(markdown, opts?)` | Replace full document. `opts.undoable=false` skips undo history entry. Use `opts.preservePreviewScroll=true` in toolbar/programmatic full-document rewrites to keep preview stable during render. |
|
|
322
|
+
| `getTokens` | `() => object[]` | Get markdown-it token array for current markdown. |
|
|
323
|
+
| `getPreview` | `() => string` | Get sanitized preview HTML. |
|
|
324
|
+
| `getSelection` | `() => { from, to, text, lineFrom, lineTo }` | Current selection info (`line*` are 0-based). |
|
|
325
|
+
| `setSelection` | `(from, to)` | Set selection by character offsets. |
|
|
326
|
+
| `insertText` | `(text, position?)` | Insert text at cursor or explicit offset. |
|
|
327
|
+
| `replaceSelection` | `(text)` | Replace current selection. |
|
|
328
|
+
| `undo` | `()` | Undo in code editor. |
|
|
329
|
+
| `redo` | `()` | Redo in code editor. |
|
|
330
|
+
| `focus` | `()` | Focus code editor. |
|
|
331
|
+
| `setMode` | `(mode)` | Switch mode: `split`, `code`, `preview`, `wysiwyg`. |
|
|
332
|
+
| `getMode` | `() => mode` | Read current mode. |
|
|
333
|
+
| `setTheme` | `(theme) => string` | Switch theme to `auto` or one of the registered built-in theme ids. |
|
|
334
|
+
| `getTheme` | `() => string` | Read current theme id. |
|
|
335
|
+
| `getAvailableThemes` | `() => { id, label, description, scheme }[]` | List built-in theme metadata for selectors/settings UIs. |
|
|
336
|
+
| `isBusy` | `() => boolean` | Returns whether any tracked async task is currently active. |
|
|
337
|
+
| `getBusyState` | `() => object` | Returns current busy state snapshot. |
|
|
338
|
+
| `beginBusyTask` | `(opts?) => string` | Start a manual busy task and return its token. |
|
|
339
|
+
| `updateBusyTask` | `(token, patch) => void` | Update message/details for a running busy task. |
|
|
340
|
+
| `endBusyTask` | `(token) => void` | End a previously started busy task. |
|
|
341
|
+
| `cancelBusyTask` | `(token?) => void` | Cancel one busy task by token, or all when omitted. |
|
|
342
|
+
| `runWithBusy` | `(task, opts?) => Promise<any>` | Wrap an async task with loading overlay, lock, and optional cancellation signal. |
|
|
343
|
+
| `registerAction` | `(actionDef)` | Register custom toolbar action. |
|
|
344
|
+
| `unregisterAction` | `(id)` | Remove custom toolbar action. |
|
|
345
|
+
| `getToolbarConfig` | `() => object \| null` | Get the current declarative toolbar config, if one is active. |
|
|
346
|
+
| `setToolbarConfig` | `(config) => void` | Replace the toolbar layout at runtime. |
|
|
347
|
+
| `updateToolbarConfig` | `(mutator) => object` | Mutate current toolbar config via callback and apply it. |
|
|
348
|
+
| `upsertToolbarGroup` | `(group) => object` | Add or replace one toolbar group by id. |
|
|
349
|
+
| `removeToolbarGroup` | `(groupId) => object` | Remove one toolbar group by id. |
|
|
350
|
+
| `upsertToolbarItem` | `(groupId, item, position?) => object` | Add or replace one top-level group item. |
|
|
351
|
+
| `removeToolbarItem` | `(groupId, itemId) => object` | Remove one top-level group item by id. |
|
|
352
|
+
| `upsertDropdownItem` | `(groupId, dropdownId, item, position?) => object` | Add or replace one dropdown entry. |
|
|
353
|
+
| `removeDropdownItem` | `(groupId, dropdownId, itemId) => object` | Remove one dropdown entry by id. |
|
|
354
|
+
| `runCommand` | `(id, args?)` | Run action by id programmatically. |
|
|
355
|
+
| `openDrawioEditor` | `(opts?) => Promise<boolean>` | Open draw.io modal and insert/update `{xml}` block line. |
|
|
356
|
+
| `proposeChange` | `(newMarkdown, opts?) => Promise<boolean>` | Open diff modal and apply if accepted. Supports `opts.mode`: `replace-all`, `replace-selection`, `insert-at-cursor`. |
|
|
357
|
+
| `getCompatibilityReport` | `() => object` | Get latest compatibility report (`disabled`, `valid`, `warning`, `invalid`). |
|
|
358
|
+
| `getCompatibilityStatus` | `() => 'disabled' \| 'valid' \| 'warning' \| 'invalid'` | Get current compatibility status. |
|
|
359
|
+
| `isCompatibilityEnabled` | `() => boolean` | Check whether compatibility mode is active. |
|
|
360
|
+
| `setCompatibilityEnabled` | `(enabled) => object` | Enable/disable compatibility mode and return latest report. |
|
|
361
|
+
| `setCompatibilityProfile` | `(profile) => object` | Replace compatibility profile and revalidate when enabled. |
|
|
362
|
+
| `validateCompatibility` | `(opts?) => object` | Run compatibility validation manually. |
|
|
363
|
+
| `proposeCompatibilityFix` | `(issueId) => Promise<boolean>` | Propose/apply one issue fix through diff modal. |
|
|
364
|
+
| `proposeAllCompatibilityFixes` | `() => Promise<boolean>` | Propose/apply one combined fix for all fixable issues. |
|
|
365
|
+
| `destroy` | `()` | Dispose editor instance and listeners. |
|
|
366
|
+
|
|
367
|
+
### Example: programmatic content proposal
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
const accepted = await editor.proposeChange('# Suggested update\n\nGenerated text...');
|
|
371
|
+
if (accepted) {
|
|
372
|
+
console.log('Applied');
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Example: wrapping custom async work with loading state
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
await editor.runWithBusy(async ({ signal, update }) => {
|
|
380
|
+
update({ label: 'Downloading template...', detail: 'Template: weekly-report' });
|
|
381
|
+
|
|
382
|
+
const response = await fetch('/api/templates/weekly-report', { signal });
|
|
383
|
+
const markdown = await response.text();
|
|
384
|
+
editor.replaceSelection(markdown);
|
|
385
|
+
}, {
|
|
386
|
+
label: 'Downloading template...',
|
|
387
|
+
detail: 'Template: weekly-report',
|
|
388
|
+
lock: true,
|
|
389
|
+
cancellable: true,
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Example: proposal apply modes
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
await editor.proposeChange('# Full replacement', { mode: 'replace-all' });
|
|
397
|
+
await editor.proposeChange('only this part', { mode: 'replace-selection' });
|
|
398
|
+
await editor.proposeChange(' inserted chunk ', { mode: 'insert-at-cursor' });
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
- `replace-selection` falls back to `insert-at-cursor` when no text is selected.
|
|
402
|
+
- In `insert-at-cursor`, insertion happens at the end of the current selection/cursor (`selection.to`).
|
|
403
|
+
|
|
404
|
+
### Theme helpers export
|
|
405
|
+
|
|
406
|
+
```js
|
|
407
|
+
import { EDITOR_THEME_PRESETS, getEditorThemeList } from 'smart-md-editor';
|
|
408
|
+
|
|
409
|
+
console.log(Object.keys(EDITOR_THEME_PRESETS));
|
|
410
|
+
console.log(getEditorThemeList());
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
If you want to add another built-in theme in the source tree, define its token set in `src/styles/themes.js`; the editor stylesheet consumes that registry automatically.
|
|
414
|
+
|
|
415
|
+
## Events and Callback Usage
|
|
416
|
+
|
|
417
|
+
### Compatibility callbacks example
|
|
418
|
+
|
|
419
|
+
```js
|
|
420
|
+
const editor = createEditor('#editor', {
|
|
421
|
+
compatibility: {
|
|
422
|
+
enabled: true,
|
|
423
|
+
},
|
|
424
|
+
onCompatibilityStatusChange(status, report) {
|
|
425
|
+
console.log(status, report.summary);
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const report = editor.validateCompatibility();
|
|
430
|
+
if (report.issues[0]?.fixable) {
|
|
431
|
+
await editor.proposeCompatibilityFix(report.issues[0].id);
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Compatibility profile example (Eleventy-like)
|
|
436
|
+
|
|
437
|
+
```js
|
|
438
|
+
import markdownItAnchor from 'markdown-it-anchor';
|
|
439
|
+
import markdownItCollapsible from 'markdown-it-collapsible';
|
|
440
|
+
|
|
441
|
+
const editor = createEditor('#editor', {
|
|
442
|
+
compatibility: {
|
|
443
|
+
enabled: true,
|
|
444
|
+
markdownIt: {
|
|
445
|
+
html: true,
|
|
446
|
+
breaks: true,
|
|
447
|
+
linkify: true,
|
|
448
|
+
},
|
|
449
|
+
disableRules: ['emphasis'],
|
|
450
|
+
plugins: [
|
|
451
|
+
[markdownItAnchor, {
|
|
452
|
+
permalink: true,
|
|
453
|
+
permalinkSymbol: '',
|
|
454
|
+
permalinkBefore: false,
|
|
455
|
+
}],
|
|
456
|
+
[markdownItCollapsible, {}],
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
The built-in Eleventy compatibility profile already includes an image resize plugin compatible with `#320px` / `#50%` markers in image alt text.
|
|
463
|
+
|
|
464
|
+
### JS callback options (`createEditor`)
|
|
465
|
+
|
|
466
|
+
```js
|
|
467
|
+
const editor = createEditor('#editor', {
|
|
468
|
+
onChange(markdown, tokens, html) {
|
|
469
|
+
// Persist markdown or update app state
|
|
470
|
+
},
|
|
471
|
+
onSelectionChange(sel) {
|
|
472
|
+
// sel: { from, to, text, lineFrom, lineTo }
|
|
473
|
+
},
|
|
474
|
+
onPreviewClick(element, range) {
|
|
475
|
+
// range: { from, to }
|
|
476
|
+
},
|
|
477
|
+
onUploadStart(file) {
|
|
478
|
+
console.log('Uploading:', file.name);
|
|
479
|
+
},
|
|
480
|
+
onUploadDone(file, value) {
|
|
481
|
+
// value is URL (upload success) or base64 fallback.
|
|
482
|
+
// Images become , other files become [file.name](...).
|
|
483
|
+
},
|
|
484
|
+
onUploadError(file, error) {
|
|
485
|
+
console.warn(error.message);
|
|
486
|
+
},
|
|
487
|
+
onCommand(id, args) {
|
|
488
|
+
console.log('Action run:', id, args);
|
|
489
|
+
},
|
|
490
|
+
onBusyChange(state) {
|
|
491
|
+
// state.busy, state.label, state.detail, state.canCancel
|
|
492
|
+
console.log('Busy:', state);
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Web Component events
|
|
498
|
+
|
|
499
|
+
`<smart-editor>` emits CustomEvents:
|
|
500
|
+
|
|
501
|
+
- `se-change`: `detail = { markdown, tokens, html }`
|
|
502
|
+
- `se-selection-change`: `detail = { from, to, text, lineFrom, lineTo }`
|
|
503
|
+
- `se-preview-click`: `detail = { element, lineRange: { from, to } }`
|
|
504
|
+
- `se-busy-change`: `detail = { busy, count, label, detail, scope, locked, canCancel, cancelToken }`
|
|
505
|
+
|
|
506
|
+
```js
|
|
507
|
+
const el = document.querySelector('smart-editor');
|
|
508
|
+
|
|
509
|
+
el.addEventListener('se-change', (e) => {
|
|
510
|
+
console.log(e.detail.markdown);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
el.addEventListener('se-preview-click', (e) => {
|
|
514
|
+
console.log(e.detail.lineRange.from);
|
|
515
|
+
});
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
## Built-in Plugins and Features
|
|
519
|
+
|
|
520
|
+
The editor auto-registers built-in toolbar actions grouped by intent.
|
|
521
|
+
|
|
522
|
+
### Inline formatting
|
|
523
|
+
|
|
524
|
+
- `bold`
|
|
525
|
+
- `italic`
|
|
526
|
+
- `strikethrough`
|
|
527
|
+
- `inline-code`
|
|
528
|
+
|
|
529
|
+
### Block structure
|
|
530
|
+
|
|
531
|
+
- `h1`, `h2`, `h3`
|
|
532
|
+
- `blockquote`
|
|
533
|
+
- `hr`
|
|
534
|
+
- `code-block`
|
|
535
|
+
|
|
536
|
+
### Lists
|
|
537
|
+
|
|
538
|
+
- `ul`
|
|
539
|
+
- `ol`
|
|
540
|
+
- `task-list`
|
|
541
|
+
|
|
542
|
+
### Insert tools
|
|
543
|
+
|
|
544
|
+
- `link`
|
|
545
|
+
- `image` (URL prompt)
|
|
546
|
+
- `asset-upload` (asset picker + paste/drop support; images -> markdown image, files -> markdown link)
|
|
547
|
+
- `table` (dialog)
|
|
548
|
+
- `mermaid`
|
|
549
|
+
- `drawio`
|
|
550
|
+
|
|
551
|
+
### Parser-level extensions included in core
|
|
552
|
+
|
|
553
|
+
- Source line mapping attributes for code-preview sync.
|
|
554
|
+
- Split-mode bidirectional smooth vertical scroll sync between code and preview (`scrollSync`, enabled by default).
|
|
555
|
+
- Table cell source-column metadata.
|
|
556
|
+
- `draw.io` image block rendering from `{xml}` with click-to-edit in preview.
|
|
557
|
+
- Fenced `mermaid` block placeholders rendered with Mermaid (if present).
|
|
558
|
+
- Inline/block math placeholders rendered with KaTeX post-processing.
|
|
559
|
+
- Image alt resize syntax: `` -> `<img width="320" height="180">`.
|
|
560
|
+
|
|
561
|
+
## Extending Functionality (Custom Toolbar Buttons)
|
|
562
|
+
|
|
563
|
+
Use `registerAction` to add custom actions to the toolbar.
|
|
564
|
+
|
|
565
|
+
## Toolbar Layout Configuration
|
|
566
|
+
|
|
567
|
+
If `toolbar` is omitted, the editor renders the legacy toolbar derived from registered actions (`group` + `order`).
|
|
568
|
+
|
|
569
|
+
If `toolbar` is provided, the toolbar becomes fully declarative: you decide which items are visible, in what order, in which group, and whether each item renders as `label`, `icon`, or `icon-label`.
|
|
570
|
+
|
|
571
|
+
### Supported item types
|
|
572
|
+
|
|
573
|
+
- Action reference: maps to a registered action by id.
|
|
574
|
+
- Custom item: defines its own `run(api, state, args?)` inline.
|
|
575
|
+
- Dropdown: groups action references and custom items under one hover/click trigger.
|
|
576
|
+
|
|
577
|
+
### Toolbar config example
|
|
578
|
+
|
|
579
|
+
```js
|
|
580
|
+
const toolbar = {
|
|
581
|
+
groups: [
|
|
582
|
+
{
|
|
583
|
+
id: 'inline',
|
|
584
|
+
order: 10,
|
|
585
|
+
items: [
|
|
586
|
+
{ action: 'bold', display: 'icon' },
|
|
587
|
+
{ action: 'italic', display: 'icon' },
|
|
588
|
+
{
|
|
589
|
+
id: 'more-inline',
|
|
590
|
+
label: 'More',
|
|
591
|
+
display: 'icon-label',
|
|
592
|
+
items: [
|
|
593
|
+
{ action: 'strikethrough', display: 'label' },
|
|
594
|
+
{ action: 'inline-code', label: 'Code', display: 'label' },
|
|
595
|
+
],
|
|
596
|
+
},
|
|
597
|
+
],
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
id: 'templates',
|
|
601
|
+
order: 20,
|
|
602
|
+
items: [
|
|
603
|
+
{
|
|
604
|
+
id: 'templates-menu',
|
|
605
|
+
label: 'Templates',
|
|
606
|
+
display: 'label',
|
|
607
|
+
items: [
|
|
608
|
+
{
|
|
609
|
+
id: 'template-news',
|
|
610
|
+
label: 'News Article',
|
|
611
|
+
args: { templateId: 'news' },
|
|
612
|
+
async run(api, state, args) {
|
|
613
|
+
const res = await fetch(`/api/templates/${args.templateId}`);
|
|
614
|
+
const { markdown } = await res.json();
|
|
615
|
+
api.setMarkdown(markdown, { preservePreviewScroll: true });
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
},
|
|
620
|
+
],
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
};
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Group schema
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
{
|
|
630
|
+
id?: string,
|
|
631
|
+
order?: number,
|
|
632
|
+
items: ToolbarItem[],
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
`toolbar.groups` accepts either an array of group objects or an object map keyed by group id.
|
|
637
|
+
|
|
638
|
+
### Item schema
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
type ToolbarDisplay = 'label' | 'icon' | 'icon-label';
|
|
642
|
+
|
|
643
|
+
type ToolbarItem =
|
|
644
|
+
| string
|
|
645
|
+
| {
|
|
646
|
+
id?: string,
|
|
647
|
+
action: string,
|
|
648
|
+
label?: string,
|
|
649
|
+
icon?: string,
|
|
650
|
+
title?: string,
|
|
651
|
+
shortcut?: string,
|
|
652
|
+
display?: ToolbarDisplay,
|
|
653
|
+
args?: object,
|
|
654
|
+
}
|
|
655
|
+
| {
|
|
656
|
+
id?: string,
|
|
657
|
+
label?: string,
|
|
658
|
+
icon?: string,
|
|
659
|
+
title?: string,
|
|
660
|
+
shortcut?: string,
|
|
661
|
+
display?: ToolbarDisplay,
|
|
662
|
+
args?: object,
|
|
663
|
+
isEnabled?: (state) => boolean,
|
|
664
|
+
isActive?: (state) => boolean,
|
|
665
|
+
run: (api, state, args?) => void | Promise<void>,
|
|
666
|
+
}
|
|
667
|
+
| {
|
|
668
|
+
id?: string,
|
|
669
|
+
label?: string,
|
|
670
|
+
icon?: string,
|
|
671
|
+
title?: string,
|
|
672
|
+
display?: ToolbarDisplay,
|
|
673
|
+
items: Array<string | object>,
|
|
674
|
+
};
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Runtime toolbar updates
|
|
678
|
+
|
|
679
|
+
Use helper methods when host data changes, for example after the user creates a new template.
|
|
680
|
+
|
|
681
|
+
Example for integrators: add a custom action button at runtime in declarative toolbar mode,
|
|
682
|
+
even if the target group is not defined in initial `toolbar.groups`.
|
|
683
|
+
|
|
684
|
+
```js
|
|
685
|
+
editor.registerAction({
|
|
686
|
+
id: 'star-wrap',
|
|
687
|
+
title: 'Add star',
|
|
688
|
+
icon: '⭐',
|
|
689
|
+
run(api, state) {
|
|
690
|
+
const text = state.selection?.text;
|
|
691
|
+
if (text) api.replaceSelection(`⭐ ${text} ⭐`);
|
|
692
|
+
else api.insertText(' ⭐ ');
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
editor.upsertToolbarItem('custom', {
|
|
697
|
+
id: 'star-wrap-item',
|
|
698
|
+
action: 'star-wrap',
|
|
699
|
+
display: 'icon',
|
|
700
|
+
});
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
`upsertToolbarItem(groupId, ...)` auto-creates the group when it does not exist.
|
|
704
|
+
If you need explicit group ordering, call `upsertToolbarGroup({ id, order, items: [] })` first.
|
|
705
|
+
|
|
706
|
+
```js
|
|
707
|
+
editor.upsertDropdownItem('templates', 'templates-menu', {
|
|
708
|
+
id: 'template-new',
|
|
709
|
+
label: 'New Template',
|
|
710
|
+
async run(api) {
|
|
711
|
+
const res = await fetch('/api/templates/new');
|
|
712
|
+
const { markdown } = await res.json();
|
|
713
|
+
api.setMarkdown(markdown, { preservePreviewScroll: true });
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
editor.removeDropdownItem('templates', 'templates-menu', 'template-new');
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Positioning is supported by optional `{ beforeId, afterId }` for `upsertToolbarItem` and `upsertDropdownItem`.
|
|
721
|
+
|
|
722
|
+
### Action schema
|
|
723
|
+
|
|
724
|
+
```ts
|
|
725
|
+
{
|
|
726
|
+
id: string,
|
|
727
|
+
label?: string,
|
|
728
|
+
icon?: string, // SVG string or text
|
|
729
|
+
title?: string,
|
|
730
|
+
group?: string, // default: 'default'
|
|
731
|
+
order?: number, // default: 50
|
|
732
|
+
shortcut?: string, // display only
|
|
733
|
+
isEnabled?: (state) => boolean,
|
|
734
|
+
isActive?: (state) => boolean,
|
|
735
|
+
run: async (api, state, args?) => void,
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### `api` object available in actions
|
|
740
|
+
|
|
741
|
+
- `getMarkdown`, `setMarkdown`
|
|
742
|
+
- `getTokens`, `getPreview`
|
|
743
|
+
- `getSelection`, `setSelection`
|
|
744
|
+
- `insertText`, `replaceSelection`
|
|
745
|
+
- `runCommand`
|
|
746
|
+
- `getToolbarConfig`, `setToolbarConfig`
|
|
747
|
+
- `updateToolbarConfig`
|
|
748
|
+
- `upsertToolbarGroup`, `removeToolbarGroup`
|
|
749
|
+
- `upsertToolbarItem`, `removeToolbarItem`
|
|
750
|
+
- `upsertDropdownItem`, `removeDropdownItem`
|
|
751
|
+
- `openDrawioEditor`
|
|
752
|
+
- `focus`
|
|
753
|
+
|
|
754
|
+
When an action rewrites the whole document (`setMarkdown(...)`), prefer:
|
|
755
|
+
|
|
756
|
+
```js
|
|
757
|
+
api.setMarkdown(nextMarkdown, { preservePreviewScroll: true });
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
This keeps preview stable during synchronous/asynchronous render work and avoids visible jump on toolbar-triggered document rewrites.
|
|
761
|
+
|
|
762
|
+
### `state` object available in actions
|
|
763
|
+
|
|
764
|
+
- `state.selection`
|
|
765
|
+
- `state.markdown`
|
|
766
|
+
- `state.cursorLine`
|
|
767
|
+
|
|
768
|
+
### Example: async custom action
|
|
769
|
+
|
|
770
|
+
```js
|
|
771
|
+
editor.registerAction({
|
|
772
|
+
id: 'insert-suggestion',
|
|
773
|
+
title: 'Insert suggestion',
|
|
774
|
+
label: 'AI',
|
|
775
|
+
group: 'custom',
|
|
776
|
+
order: 200,
|
|
777
|
+
async run(api, state) {
|
|
778
|
+
const res = await fetch('/api/suggest', {
|
|
779
|
+
method: 'POST',
|
|
780
|
+
headers: { 'Content-Type': 'application/json' },
|
|
781
|
+
body: JSON.stringify({ markdown: state.markdown }),
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
const { suggestion } = await res.json();
|
|
785
|
+
api.replaceSelection(suggestion || 'No suggestion');
|
|
786
|
+
},
|
|
787
|
+
});
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
To remove it later:
|
|
791
|
+
|
|
792
|
+
```js
|
|
793
|
+
editor.unregisterAction('insert-suggestion');
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
## Markdown Compatibility Notes
|
|
797
|
+
|
|
798
|
+
The editor is designed to keep markdown output compatible with markdown-it based pipelines.
|
|
799
|
+
|
|
800
|
+
- Core parser is markdown-it with configurable options/plugins.
|
|
801
|
+
- Generated markdown remains plain markdown text.
|
|
802
|
+
- Mermaid integration is represented as fenced blocks.
|
|
803
|
+
- draw.io integration is represented as `{xml}` lines.
|
|
804
|
+
- Image resizing metadata is encoded in alt text using `|WxH` suffix.
|
|
805
|
+
|
|
806
|
+
## draw.io Markdown Format
|
|
807
|
+
|
|
808
|
+
draw.io diagrams are serialized as one markdown line:
|
|
809
|
+
|
|
810
|
+
```md
|
|
811
|
+
{<uri-encoded-xml>}
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
- `<image-src>` is typically a `data:image/svg+xml;base64,...` preview image.
|
|
815
|
+
- `<uri-encoded-xml>` is diagram XML encoded with `encodeURIComponent`.
|
|
816
|
+
- In preview, clicking the image or the `Edit diagram` button opens draw.io modal and preserves XML.
|
|
817
|
+
|
|
818
|
+
### draw.io URL fallback behavior
|
|
819
|
+
|
|
820
|
+
By default, editor starts draw.io modal with hosted embed (`https://embed.diagrams.net/?...`).
|
|
821
|
+
If you provide a custom local `drawio.url` and init fails, it retries once with hosted embed.
|
|
822
|
+
|
|
823
|
+
Use this option to enforce strict offline behavior:
|
|
824
|
+
|
|
825
|
+
```js
|
|
826
|
+
createEditor(element, {
|
|
827
|
+
drawio: {
|
|
828
|
+
url: '/drawio/?embed=1&proto=json&spin=1&ui=min&libraries=1',
|
|
829
|
+
allowHostedFallback: false,
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### Self-hosted draw.io assets (optional)
|
|
835
|
+
|
|
836
|
+
The npm package ships editor code only (no bundled `dist/drawio` webapp). For offline/self-hosted mode,
|
|
837
|
+
download draw.io assets directly to your application and point `drawio.url` at that location.
|
|
838
|
+
|
|
839
|
+
```bash
|
|
840
|
+
npx smart-md-editor drawio:download --out ./public/drawio --version latest
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
Static hosting examples:
|
|
844
|
+
|
|
845
|
+
```bash
|
|
846
|
+
# Vite / plain static app
|
|
847
|
+
npx smart-md-editor drawio:download --out ./public/drawio --version latest
|
|
848
|
+
|
|
849
|
+
# Next.js
|
|
850
|
+
npx smart-md-editor drawio:download --out ./public/drawio --version latest
|
|
851
|
+
|
|
852
|
+
# Any app served from a subpath, e.g. https://example.com/docs/
|
|
853
|
+
npx smart-md-editor drawio:download --out ./public/docs/drawio --version latest
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
URL mapping:
|
|
857
|
+
|
|
858
|
+
```txt
|
|
859
|
+
./public/drawio -> /drawio/?embed=1&proto=json&spin=1&ui=min&libraries=1
|
|
860
|
+
./public/docs/drawio -> /docs/drawio/?embed=1&proto=json&spin=1&ui=min&libraries=1
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
Then configure editor:
|
|
864
|
+
|
|
865
|
+
```js
|
|
866
|
+
createEditor(element, {
|
|
867
|
+
drawio: {
|
|
868
|
+
url: '/drawio/?embed=1&proto=json&spin=1&ui=min&libraries=1',
|
|
869
|
+
allowHostedFallback: false,
|
|
870
|
+
},
|
|
871
|
+
});
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
### Example
|
|
875
|
+
|
|
876
|
+
```md
|
|
877
|
+
{%3Cmxfile%20host%3D%22app.diagrams.net%22%3E%3Cdiagram%20id%3D%22d1%22%20name%3D%22Page-1%22%3E%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E%3C%2Fdiagram%3E%3C%2Fmxfile%3E}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
## Security Notes
|
|
881
|
+
|
|
882
|
+
- Preview HTML is sanitized with DOMPurify before rendering.
|
|
883
|
+
- If you add custom parser output attributes/tags needed in preview, update the allowlist in `src/ui/PreviewPanel.js`.
|
|
884
|
+
- Upload endpoint must validate file type/size server-side as well.
|
|
885
|
+
|
|
886
|
+
## License
|
|
887
|
+
|
|
888
|
+
This project is licensed under the MIT License. See `LICENSE` for details.
|
|
889
|
+
|
|
890
|
+
## Versioned API Changes
|
|
891
|
+
|
|
892
|
+
Use this section as a compatibility reference when upgrading the editor in host applications.
|
|
893
|
+
|
|
894
|
+
### `0.1.0`
|
|
895
|
+
|
|
896
|
+
- Initial public integration surface:
|
|
897
|
+
- Factory: `createEditor(element, options)`
|
|
898
|
+
- Exports: `EditorCore`, `SmartEditorElement`
|
|
899
|
+
- Web Component registration: `<smart-editor>`
|
|
900
|
+
- Runtime API includes document ops, selection ops, mode switching, action registration, draw.io modal, and diff-based `proposeChange`.
|
|
901
|
+
- Core options include markdown-it configuration, upload configuration, draw.io URL override, and integration callbacks.
|
|
902
|
+
- Built-in action groups include inline formatting, blocks, lists, links/images, table/mermaid/draw.io, and image upload.
|
|
903
|
+
- Parser support includes source-line mapping, table cell metadata, Mermaid/KaTeX placeholders, draw.io image+xml blocks, and image dimension syntax (`|WxH`).
|
|
904
|
+
|
|
905
|
+
### Upgrade Notes
|
|
906
|
+
|
|
907
|
+
- Treat any removal or signature change in methods listed under `Runtime API` as breaking.
|
|
908
|
+
- Treat callback signature changes in `Configuration Options` as breaking.
|
|
909
|
+
- Treat changes to markdown serialization conventions (`draw.io` image+xml block, image `|WxH`) as breaking for downstream pipelines.
|
|
910
|
+
- Prefer additive changes for custom action integrations: add new action IDs instead of mutating existing IDs used by host automation.
|
|
911
|
+
|
|
912
|
+
### `0.2.0`
|
|
913
|
+
|
|
914
|
+
- Added declarative `toolbar` config for explicit grouping, ordering, display mode selection, and dropdown menus.
|
|
915
|
+
- Added runtime toolbar methods: `getToolbarConfig()` and `setToolbarConfig(config)`.
|
|
916
|
+
- Toolbar items now support inline async `run(api, state, args?)` handlers in addition to references to registered actions.
|
|
917
|
+
- Added runtime toolbar helper methods for granular updates (`updateToolbarConfig`, `upsert/remove` for groups/items/dropdown items).
|
|
918
|
+
|
|
919
|
+
## Development Commands
|
|
920
|
+
|
|
921
|
+
```bash
|
|
922
|
+
npm install
|
|
923
|
+
npm run build
|
|
924
|
+
npm run dev
|
|
925
|
+
npm run drawio:download
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
To run the demo, serve from repository root (not from `demo/`):
|
|
929
|
+
|
|
930
|
+
```bash
|
|
931
|
+
npx serve .
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
Then open `/demo/` in the browser.
|
|
935
|
+
|
|
936
|
+
## Troubleshooting
|
|
937
|
+
|
|
938
|
+
### Editor is blank or layout is broken
|
|
939
|
+
|
|
940
|
+
Ensure the editor container has explicit height, for example:
|
|
941
|
+
|
|
942
|
+
```html
|
|
943
|
+
<div id="editor" style="height: 600px;"></div>
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### Mermaid blocks stay as raw code
|
|
947
|
+
|
|
948
|
+
Ensure Mermaid script is loaded and initialized on the page (`window.mermaid`).
|
|
949
|
+
|
|
950
|
+
### Math blocks render as placeholders or plain text
|
|
951
|
+
|
|
952
|
+
Ensure KaTeX CSS is loaded. (KaTeX rendering is run by core, CSS controls visual output.)
|
|
953
|
+
|
|
954
|
+
### Assets do not upload
|
|
955
|
+
|
|
956
|
+
For **images**, if `upload.endpoint` is missing or the upload request fails, the editor falls back to embedding the image as a base64 data URI (``), so the document remains self-contained.
|
|
957
|
+
|
|
958
|
+
For **non-image files** (PDF, Word, Excel, etc.), there is no base64 fallback — a data-URI makes no sense as a markdown link and would bloat the document. If no endpoint resolves for a non-image file, `onUploadError` is fired and nothing is inserted.
|
|
959
|
+
|
|
960
|
+
If your storage service uses **different endpoints per resource type** (e.g. Cloudinary's `/image/upload` vs `/raw/upload`), use `upload.endpoints`:
|
|
961
|
+
|
|
962
|
+
```js
|
|
963
|
+
upload: {
|
|
964
|
+
endpoint: 'https://api.cloudinary.com/v1_1/demo/raw/upload', // default for everything
|
|
965
|
+
endpoints: {
|
|
966
|
+
'image/*': 'https://api.cloudinary.com/v1_1/demo/image/upload', // images go here instead
|
|
967
|
+
},
|
|
968
|
+
extraFields: { upload_preset: 'my_preset' },
|
|
969
|
+
}
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### React usage
|
|
973
|
+
|
|
974
|
+
A React adapter exists at `src/adapters/react/SmartEditor.jsx`. It exports `SmartEditor`. In the current build exports, the main package entry exports `createEditor`, `EditorCore`, and `SmartEditorElement`.
|