@synclineapi/mdx-editor 0.1.2 → 1.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.
- package/README.md +403 -139
- package/dist/core/config/EditorConfig.d.ts +5 -0
- package/dist/core/config/defaults.d.ts +33 -0
- package/dist/core/config/index.d.ts +2 -0
- package/dist/core/dom/editor-pane.d.ts +7 -0
- package/dist/core/dom/editor-root.d.ts +8 -0
- package/dist/core/dom/index.d.ts +11 -0
- package/dist/core/dom/preview-pane.d.ts +5 -0
- package/dist/core/dom/skip-link.d.ts +1 -0
- package/dist/core/dom/status-bar.d.ts +13 -0
- package/dist/core/dom/toolbar-dom.d.ts +5 -0
- package/dist/core/editor.d.ts +139 -7
- package/dist/core/mdx-themes.d.ts +3 -0
- package/dist/core/platform.d.ts +31 -0
- package/dist/core/plugin-manager.d.ts +1 -24
- package/dist/core/plugins/PluginContext.d.ts +26 -0
- package/dist/core/plugins/PluginManager.d.ts +41 -0
- package/dist/core/plugins/index.d.ts +3 -0
- package/dist/core/renderer/CodeRenderer.d.ts +4 -0
- package/dist/core/renderer/ListRenderer.d.ts +5 -0
- package/dist/core/renderer/MarkdownRenderer.d.ts +7 -0
- package/dist/core/renderer/MdxValidator.d.ts +3 -0
- package/dist/core/renderer/Renderer.d.ts +10 -0
- package/dist/core/renderer/TableRenderer.d.ts +4 -0
- package/dist/core/renderer/index.d.ts +7 -0
- package/dist/core/renderer/sanitize.d.ts +1 -0
- package/dist/core/renderer.d.ts +6 -31
- package/dist/core/toolbar.d.ts +1 -0
- package/dist/core/types.d.ts +150 -2
- package/dist/core/ui/AutocompleteController.d.ts +58 -0
- package/dist/core/ui/DropdownController.d.ts +8 -0
- package/dist/core/ui/FindReplaceController.d.ts +60 -0
- package/dist/core/ui/LineNumberController.d.ts +218 -0
- package/dist/core/ui/ModeController.d.ts +9 -0
- package/dist/core/ui/PreviewController.d.ts +13 -0
- package/dist/core/ui/ResponsiveController.d.ts +30 -0
- package/dist/core/ui/SplitterController.d.ts +31 -0
- package/dist/core/ui/StatusBarController.d.ts +16 -0
- package/dist/core/ui/ThemeController.d.ts +8 -0
- package/dist/core/ui/ToolbarController.d.ts +10 -0
- package/dist/core/ui/UIController.d.ts +43 -0
- package/dist/core/ui/index.d.ts +12 -0
- package/dist/index.d.ts +29 -1
- package/dist/plugins/basic-formatting/index.d.ts +1 -0
- package/dist/plugins/callouts/index.d.ts +1 -0
- package/dist/plugins/diagrams/index.d.ts +1 -0
- package/dist/plugins/index.d.ts +1 -11
- package/dist/plugins/layout/index.d.ts +3 -0
- package/dist/plugins/lists/index.d.ts +1 -0
- package/dist/plugins/media/index.d.ts +2 -0
- package/dist/plugins/token-utils.d.ts +49 -0
- package/dist/plugins/utilities/index.d.ts +5 -0
- package/dist/style.css +1 -1
- package/dist/syncline-mdx-editor.js +9628 -1835
- package/dist/syncline-mdx-editor.umd.cjs +988 -90
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
# @synclineapi/mdx-editor
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@synclineapi/mdx-editor)
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
4
|
[](https://www.typescriptlang.org/)
|
|
6
5
|
[](https://www.w3.org/TR/WCAG21/)
|
|
7
6
|
[](https://markdown.synclineapi.com/)
|
|
8
7
|
|
|
9
|
-
A **framework-agnostic**, plugin-based MDX/Markdown editor with
|
|
8
|
+
A **framework-agnostic**, plugin-based MDX/Markdown editor with a high-performance virtual-rendering code pane, live HTML preview, a configurable toolbar, and 38 built-in content plugins. Works with React, Vue, Angular, Next.js, Svelte, or plain JavaScript.
|
|
10
9
|
|
|
11
10
|
**[Live Demo →](https://markdown.synclineapi.com/)**
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
17
|
npm install @synclineapi/mdx-editor
|
|
17
18
|
```
|
|
18
19
|
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
19
24
|
### Vanilla JavaScript
|
|
20
25
|
|
|
21
26
|
```html
|
|
22
|
-
<div id="editor"></div>
|
|
27
|
+
<div id="editor" style="height: 600px;"></div>
|
|
23
28
|
|
|
24
29
|
<script type="module">
|
|
25
30
|
import { createEditor } from '@synclineapi/mdx-editor';
|
|
@@ -27,15 +32,36 @@ npm install @synclineapi/mdx-editor
|
|
|
27
32
|
|
|
28
33
|
const editor = createEditor({
|
|
29
34
|
container: '#editor',
|
|
30
|
-
value: '# Hello World',
|
|
31
|
-
onChange: (markdown) => console.log(
|
|
35
|
+
value: '# Hello World\n\nStart writing *MDX* here.',
|
|
36
|
+
onChange: (markdown) => console.log(markdown),
|
|
32
37
|
});
|
|
33
38
|
</script>
|
|
34
39
|
```
|
|
35
40
|
|
|
41
|
+
### React
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useEffect, useRef } from 'react';
|
|
45
|
+
import { createEditor, type SynclineMDXEditor } from '@synclineapi/mdx-editor';
|
|
46
|
+
import '@synclineapi/mdx-editor/style.css';
|
|
47
|
+
|
|
48
|
+
export function Editor({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
|
49
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
50
|
+
const editor = useRef<SynclineMDXEditor | null>(null);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!ref.current) return;
|
|
54
|
+
editor.current = createEditor({ container: ref.current, value, onChange });
|
|
55
|
+
return () => editor.current?.destroy();
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
return <div ref={ref} style={{ height: 600 }} />;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
36
62
|
### Custom Plugin Selection
|
|
37
63
|
|
|
38
|
-
```
|
|
64
|
+
```ts
|
|
39
65
|
import { SynclineMDXEditor, boldPlugin, italicPlugin, headingPlugin } from '@synclineapi/mdx-editor';
|
|
40
66
|
import '@synclineapi/mdx-editor/style.css';
|
|
41
67
|
|
|
@@ -46,83 +72,349 @@ const editor = new SynclineMDXEditor({
|
|
|
46
72
|
});
|
|
47
73
|
```
|
|
48
74
|
|
|
49
|
-
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Features
|
|
78
|
+
|
|
79
|
+
### 38 Built-in Plugins
|
|
50
80
|
|
|
51
81
|
| Category | Plugins |
|
|
52
82
|
|----------|---------|
|
|
53
|
-
| **Formatting** | Heading (
|
|
83
|
+
| **Formatting** | Heading (H1–H6), Bold, Italic, Strikethrough, Quote |
|
|
54
84
|
| **Links & Media** | Link, Image, Image Background, Image Frame |
|
|
55
|
-
| **Code** | Inline Code, Code Block
|
|
85
|
+
| **Code** | Inline Code, Fenced Code Block |
|
|
56
86
|
| **Lists** | Unordered List, Ordered List, Task List |
|
|
57
|
-
| **Layout** | Table, Multi-Column (2–5), Tabs, Container |
|
|
87
|
+
| **Layout** | Table, Multi-Column (2–5 cols), Tabs, Container |
|
|
58
88
|
| **Components** | Accordion, Accordion Group, Card, Card Group, Steps |
|
|
59
|
-
| **Callouts** | Admonition (Tip/Warning/Caution/Danger/
|
|
60
|
-
| **Rich Content** | Highlight (8
|
|
89
|
+
| **Callouts** | Admonition (Tip / Warning / Caution / Danger / Info / Note), Tip (Good / Bad / Info) |
|
|
90
|
+
| **Rich Content** | Highlight (8 colours), Emoji, Formula (KaTeX inline & block), Tooltip, Copy Text |
|
|
91
|
+
| **Inline labels** | Badge (success / warning / error / info), Status Tag (live / beta / soon / archived) |
|
|
61
92
|
| **Embeds** | YouTube, Video file, GitHub Gist, Twitter/X, CodePen, CodeSandbox |
|
|
62
93
|
| **Diagrams** | Mermaid (Flowchart, Sequence, Class, State, ER, Journey, Gantt) |
|
|
63
94
|
| **Insert** | API Endpoint, Markdown Import, Code Snippet |
|
|
64
95
|
|
|
65
|
-
|
|
96
|
+
### Editor Engine
|
|
97
|
+
|
|
98
|
+
- **Virtual rendering** — only visible rows are in the DOM; handles documents of 10 000+ lines at 60 fps
|
|
99
|
+
- **Word wrap** — proper pixel-width wrap with full active-line highlight across all visual rows
|
|
100
|
+
- **MDX autocomplete** — context-aware component-tag and attribute suggestions; Tab-trigger snippet expansion; plugins contribute items declaratively via `completions: [...]` or dynamically via `ctx.registerCompletion()`
|
|
101
|
+
- **Semantic syntax highlighting** — every built-in plugin declares a `provideTokens` function mapping its MDX syntax to semantic token classes (`kw`, `cls`, `fn`, `str`, `op`, `typ`, `num`, `cmt`); consumers can extend this with `registerSyntaxHighlighter()`
|
|
102
|
+
- **Live HTML preview** — side-by-side split / editor-only / preview-only modes with a draggable splitter
|
|
103
|
+
- **Table of Contents** — auto-generated from headings, collapsible panel
|
|
104
|
+
- **Theme system** — light / dark toggle; auto-syncs with `data-theme`, `.smdx-dark`, or `data-color-scheme`
|
|
105
|
+
|
|
106
|
+
---
|
|
66
107
|
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
### `EditorConfig`
|
|
111
|
+
|
|
112
|
+
| Option | Type | Default | Description |
|
|
113
|
+
|--------|------|---------|-------------|
|
|
114
|
+
| `container` | `HTMLElement \| string` | — | **Required.** DOM element or CSS selector to mount into. |
|
|
115
|
+
| `value` | `string` | `''` | Initial markdown content. |
|
|
116
|
+
| `onChange` | `(value: string) => void` | — | Called on every content change (typing, paste, undo, `setValue()`). |
|
|
117
|
+
| `plugins` | `EditorPlugin[]` | all built-ins | Plugins to register. |
|
|
118
|
+
| `toolbar` | `ToolbarConfig` | default toolbar | Toolbar layout (rows of item IDs, groups, dividers). |
|
|
119
|
+
| `theme` | `Record<string, string>` | — | CSS custom property overrides for the MDX prose shell. |
|
|
120
|
+
| `mode` | `'split' \| 'editor' \| 'preview'` | `'split'` | Initial layout mode. |
|
|
121
|
+
| `placeholder` | `string` | — | Placeholder shown in an empty editor. |
|
|
122
|
+
| `readOnly` | `boolean` | `false` | Prevent all content mutations. |
|
|
123
|
+
| `renderers` | `Record<string, RendererFn>` | — | Custom preview renderer overrides. |
|
|
124
|
+
| `locale` | `Partial<EditorLocale>` | — | i18n label overrides. |
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Theming
|
|
128
|
+
|
|
129
|
+
### CSS custom properties
|
|
130
|
+
|
|
131
|
+
```css
|
|
132
|
+
.smdx-editor {
|
|
133
|
+
--smdx-primary: #6366f1; /* accent colour */
|
|
134
|
+
--smdx-bg: #ffffff; /* prose shell background */
|
|
135
|
+
--smdx-text: #1e293b; /* prose text */
|
|
136
|
+
--smdx-border: #e2e8f0; /* dividers */
|
|
137
|
+
--smdx-font-family: 'Inter', sans-serif;
|
|
138
|
+
--smdx-font-mono: 'JetBrains Mono', monospace;
|
|
139
|
+
--smdx-radius: 12px;
|
|
140
|
+
}
|
|
67
141
|
```
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
142
|
+
|
|
143
|
+
### Via config
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
createEditor({
|
|
147
|
+
container: '#editor',
|
|
148
|
+
theme: {
|
|
149
|
+
'--smdx-primary': '#e11d48',
|
|
150
|
+
'--smdx-bg': '#0f172a',
|
|
151
|
+
},
|
|
152
|
+
});
|
|
77
153
|
```
|
|
78
154
|
|
|
155
|
+
### Dark mode
|
|
156
|
+
|
|
157
|
+
Add `.smdx-dark` to the editor root, any ancestor element, `<body>`, or `<html>` — the editor observes all of them. Alternatively set `data-theme="dark"` on any ancestor (Next.js / Nuxt / Tailwind pattern):
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// CSS class toggle
|
|
161
|
+
editor.getRoot().classList.add('smdx-dark');
|
|
162
|
+
|
|
163
|
+
// data-theme attribute (works on any ancestor)
|
|
164
|
+
document.documentElement.dataset.theme = 'dark';
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Syntax Highlighting
|
|
170
|
+
|
|
171
|
+
### `registerSyntaxHighlighter()`
|
|
172
|
+
|
|
173
|
+
Add custom token providers that colour your own MDX component syntax on top of the built-in highlighting. Every built-in plugin already declares a `provideTokens` function; this API lets you extend it from outside.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import type { PluginTokenProvider } from '@synclineapi/mdx-editor';
|
|
177
|
+
import { componentTagTokens } from '@synclineapi/mdx-editor';
|
|
178
|
+
|
|
179
|
+
// Quickest option — use the built-in JSX helper
|
|
180
|
+
// Highlights <MyCard …> tag names as `cls`, attribute names as `fn`, values as `str`.
|
|
181
|
+
// Handles boolean attributes like `defaultOpen` and `disabled` automatically.
|
|
182
|
+
editor.registerSyntaxHighlighter(componentTagTokens(['MyCard', 'MyCardGroup']));
|
|
183
|
+
|
|
184
|
+
// Custom provider for non-JSX syntax
|
|
185
|
+
const myProvider: PluginTokenProvider = (line) => {
|
|
186
|
+
const segs = [];
|
|
187
|
+
// Highlight :::type admonition-style fences
|
|
188
|
+
const m = line.match(/^(:::)(\w+)/);
|
|
189
|
+
if (m) {
|
|
190
|
+
segs.push({ cls: 'kw', start: 0, end: 3 });
|
|
191
|
+
segs.push({ cls: 'cls', start: 3, end: 3 + m[2].length });
|
|
192
|
+
}
|
|
193
|
+
return segs;
|
|
194
|
+
};
|
|
195
|
+
editor.registerSyntaxHighlighter(myProvider);
|
|
196
|
+
|
|
197
|
+
// Pass an array to register several at once
|
|
198
|
+
editor.registerSyntaxHighlighter([providerA, providerB]);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Token classes
|
|
202
|
+
|
|
203
|
+
| `cls` | Colour role | Typical MDX use |
|
|
204
|
+
|-------|-------------|-----------------|
|
|
205
|
+
| `kw` | purple | Heading `#`, code fences ` ``` `, `:::`, `---`, blockquotes `>` |
|
|
206
|
+
| `str` | green | Link URLs, attribute values, inline `` `code` ``, `$math$` |
|
|
207
|
+
| `cmt` | muted gray | ~~Strikethrough~~ text |
|
|
208
|
+
| `fn` | blue | Link labels, *italic*, attribute names |
|
|
209
|
+
| `num` | orange | **Bold** text, `$$formula$$` markers |
|
|
210
|
+
| `cls` | yellow | `<Card>`, `<Tabs>`, `<Accordion>` — JSX component tag names |
|
|
211
|
+
| `op` | cyan | List markers `- 1.`, table pipes `\|` |
|
|
212
|
+
| `typ` | blue | Code-fence language identifiers (`mermaid`, `ts`, `python`) |
|
|
213
|
+
| `dec` | red | Decorators (reserved for custom use) |
|
|
214
|
+
|
|
215
|
+
### `componentTagTokens()` helper
|
|
216
|
+
|
|
217
|
+
The built-in JSX highlighter factory. Handles:
|
|
218
|
+
|
|
219
|
+
- **Tag names**: `<Card`, `</Card>`, `<Card />` → `cls`
|
|
220
|
+
- **Value attributes**: `title="…"`, `src="…"` → name as `fn`, value as `str`
|
|
221
|
+
- **Boolean attributes**: `defaultOpen`, `disabled`, `open` (no `=`) → `fn`
|
|
222
|
+
- **Multi-line tags**: attributes that continue on the next line(s) are highlighted correctly — the provider tracks open-tag state across lines.
|
|
223
|
+
- Attribute scanning is scoped to the region between the tag name and `>` so tag body text is never falsely coloured.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { componentTagTokens } from '@synclineapi/mdx-editor';
|
|
227
|
+
|
|
228
|
+
provideTokens: componentTagTokens(['Accordion', 'AccordionGroup']),
|
|
229
|
+
// <Accordion title="…" defaultOpen> → Accordion=cls title=fn "…"=str defaultOpen=fn
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Autocomplete
|
|
235
|
+
|
|
236
|
+
### `registerAutoComplete()`
|
|
237
|
+
|
|
238
|
+
Add custom autocomplete items on top of the built-in MDX completions. Items appear in the popup immediately and persist for the lifetime of the editor.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import type { CompletionItem } from '@synclineapi/mdx-editor';
|
|
242
|
+
|
|
243
|
+
editor.registerAutoComplete([
|
|
244
|
+
// Plain word completion
|
|
245
|
+
{ label: 'MyCard', kind: 'cls', detail: 'component' },
|
|
246
|
+
|
|
247
|
+
// Snippet — Tab or click inserts the full body; $1 is the cursor position
|
|
248
|
+
{
|
|
249
|
+
label: 'mycard',
|
|
250
|
+
kind: 'snip',
|
|
251
|
+
detail: '<MyCard> component',
|
|
252
|
+
description: 'Inserts a custom card block.',
|
|
253
|
+
body: '<MyCard title="$1">\n $2\n</MyCard>',
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Call it again at any time to add more items — they accumulate:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
editor.registerAutoComplete({ label: 'MyBanner', kind: 'cls', detail: 'component' });
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Completion kinds
|
|
265
|
+
|
|
266
|
+
| `kind` | Badge | Use for |
|
|
267
|
+
|--------|-------|---------|
|
|
268
|
+
| `'cls'` | **C** | Component names (`<MyCard>`, `<Tabs>`) |
|
|
269
|
+
| `'fn'` | **f** | Functions / methods |
|
|
270
|
+
| `'kw'` | **K** | Keywords |
|
|
271
|
+
| `'var'` | **·** | Attributes, variables, props |
|
|
272
|
+
| `'typ'` | **T** | Types / interfaces |
|
|
273
|
+
| `'snip'` | **S** | Snippet template — set `body` to expand on accept |
|
|
274
|
+
|
|
275
|
+
### Snippet bodies
|
|
276
|
+
|
|
277
|
+
Use `$1`, `$2`, … as cursor tab stops. The cursor lands on `$1` after expansion:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
{
|
|
281
|
+
label: 'endpoint',
|
|
282
|
+
kind: 'snip',
|
|
283
|
+
detail: 'API endpoint block',
|
|
284
|
+
body: '<Endpoint method="$1GET" path="$2/api/resource">\n $3Description\n</Endpoint>',
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
79
290
|
## Plugin API
|
|
80
291
|
|
|
81
|
-
|
|
292
|
+
Plugins are the recommended way to bundle a toolbar button, keyboard shortcut, preview renderer, and autocomplete items together as one reusable unit.
|
|
82
293
|
|
|
83
294
|
```ts
|
|
84
295
|
import type { EditorPlugin } from '@synclineapi/mdx-editor';
|
|
85
296
|
|
|
86
297
|
const myPlugin: EditorPlugin = {
|
|
87
298
|
name: 'my-custom-block',
|
|
299
|
+
|
|
300
|
+
// Toolbar button
|
|
88
301
|
toolbarItems: [{
|
|
89
302
|
id: 'myBlock',
|
|
90
303
|
label: 'My Block',
|
|
91
304
|
icon: '<svg>...</svg>',
|
|
92
|
-
tooltip: 'Insert my block',
|
|
305
|
+
tooltip: 'Insert my block (Ctrl+Shift+M)',
|
|
93
306
|
action: ({ editor }) => {
|
|
94
307
|
editor.insertBlock('<MyBlock>\n Content\n</MyBlock>');
|
|
95
308
|
},
|
|
96
309
|
}],
|
|
310
|
+
|
|
311
|
+
// Autocomplete — same content as the toolbar action, now also reachable
|
|
312
|
+
// by typing the trigger word in the editor
|
|
313
|
+
completions: [
|
|
314
|
+
{
|
|
315
|
+
label: 'myblock',
|
|
316
|
+
kind: 'snip',
|
|
317
|
+
detail: '<MyBlock> component',
|
|
318
|
+
body: '<MyBlock>\n $1Content\n</MyBlock>',
|
|
319
|
+
},
|
|
320
|
+
// Component name entry (shows in autocomplete as a class suggestion)
|
|
321
|
+
{ label: 'MyBlock', kind: 'cls', detail: 'custom block component' },
|
|
322
|
+
],
|
|
323
|
+
|
|
324
|
+
// Preview renderer
|
|
97
325
|
renderers: [{
|
|
98
326
|
name: 'myBlock',
|
|
99
327
|
pattern: /<MyBlock>([\s\S]*?)<\/MyBlock>/g,
|
|
100
328
|
render: (match) => {
|
|
101
329
|
const m = match.match(/<MyBlock>([\s\S]*?)<\/MyBlock>/);
|
|
102
|
-
return `<div class="my-block">${m?.[1]
|
|
330
|
+
return `<div class="my-block">${m?.[1] ?? ''}</div>`;
|
|
103
331
|
},
|
|
104
332
|
}],
|
|
333
|
+
|
|
334
|
+
// Syntax highlighting — called per-line, highlight component tag
|
|
335
|
+
// names, attribute names, and attribute values automatically.
|
|
336
|
+
// Use the built-in helper for JSX components:
|
|
337
|
+
provideTokens: componentTagTokens(['MyBlock']),
|
|
338
|
+
// Or write a custom provider for any syntax pattern:
|
|
339
|
+
// provideTokens: (line) => {
|
|
340
|
+
// const segs = [];
|
|
341
|
+
// const m = line.match(/^(:::)(\w+)/);
|
|
342
|
+
// if (m) {
|
|
343
|
+
// segs.push({ cls: 'kw', start: 0, end: 3 });
|
|
344
|
+
// segs.push({ cls: 'cls', start: 3, end: 3 + m[2].length });
|
|
345
|
+
// }
|
|
346
|
+
// return segs;
|
|
347
|
+
// },
|
|
348
|
+
|
|
105
349
|
shortcuts: [
|
|
106
|
-
{
|
|
350
|
+
{
|
|
351
|
+
key: 'Ctrl+Shift+m',
|
|
352
|
+
action: ({ editor }) => editor.insertBlock('<MyBlock>\n Content\n</MyBlock>'),
|
|
353
|
+
description: 'Insert custom block',
|
|
354
|
+
},
|
|
107
355
|
],
|
|
108
|
-
|
|
356
|
+
|
|
357
|
+
styles: `.my-block { border: 2px solid #6366f1; padding: 16px; border-radius: 8px; }`,
|
|
358
|
+
};
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
You can also register completions dynamically inside `init()` using `ctx.registerCompletion()`:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
const myPlugin: EditorPlugin = {
|
|
365
|
+
name: 'dynamic-completions',
|
|
366
|
+
async init(ctx) {
|
|
367
|
+
const components = await fetchMyComponents(); // dynamic list
|
|
368
|
+
for (const comp of components) {
|
|
369
|
+
ctx.registerCompletion({
|
|
370
|
+
label: comp.tag,
|
|
371
|
+
kind: 'cls',
|
|
372
|
+
detail: comp.description,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
},
|
|
109
376
|
};
|
|
110
377
|
```
|
|
111
378
|
|
|
379
|
+
### Completion merge order
|
|
380
|
+
|
|
381
|
+
Every time a plugin is registered or unregistered the autocomplete list is rebuilt:
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
built-in MDX completions (tags, JSX attributes)
|
|
385
|
+
+ built-in MDX snippets (toolbar-matched snippet bodies)
|
|
386
|
+
+ plugin completions (all registered plugins, in order)
|
|
387
|
+
+ user completions (registerAutoComplete() calls)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
112
392
|
## Toolbar Customization
|
|
113
393
|
|
|
114
394
|
### Flat toolbar
|
|
115
395
|
|
|
116
|
-
```
|
|
396
|
+
```ts
|
|
117
397
|
createEditor({
|
|
118
398
|
container: '#editor',
|
|
119
399
|
toolbar: [['bold', 'italic', '|', 'heading', '|', 'link', 'image']],
|
|
120
400
|
});
|
|
121
401
|
```
|
|
122
402
|
|
|
403
|
+
### Multi-row toolbar
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
createEditor({
|
|
407
|
+
container: '#editor',
|
|
408
|
+
toolbar: [
|
|
409
|
+
['heading', '|', 'bold', 'italic', 'strikethrough'],
|
|
410
|
+
['link', 'image', '|', 'table', 'code'],
|
|
411
|
+
],
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
123
415
|
### Nested dropdowns
|
|
124
416
|
|
|
125
|
-
```
|
|
417
|
+
```ts
|
|
126
418
|
createEditor({
|
|
127
419
|
container: '#editor',
|
|
128
420
|
toolbar: [[
|
|
@@ -137,91 +429,81 @@ createEditor({
|
|
|
137
429
|
});
|
|
138
430
|
```
|
|
139
431
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Override CSS custom properties:
|
|
143
|
-
|
|
144
|
-
```css
|
|
145
|
-
.smdx-editor {
|
|
146
|
-
--smdx-primary: #e11d48;
|
|
147
|
-
--smdx-bg: #0f172a;
|
|
148
|
-
--smdx-text: #e2e8f0;
|
|
149
|
-
--smdx-border: #334155;
|
|
150
|
-
--smdx-font-family: 'Inter', sans-serif;
|
|
151
|
-
--smdx-font-mono: 'JetBrains Mono', monospace;
|
|
152
|
-
--smdx-radius: 12px;
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Or pass theme via config:
|
|
157
|
-
|
|
158
|
-
```js
|
|
159
|
-
createEditor({
|
|
160
|
-
container: '#editor',
|
|
161
|
-
theme: {
|
|
162
|
-
'--smdx-primary': '#e11d48',
|
|
163
|
-
'--smdx-bg': '#0f172a',
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Add `.smdx-dark` class for dark mode:
|
|
169
|
-
|
|
170
|
-
```js
|
|
171
|
-
editor.getRoot().classList.add('smdx-dark');
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
## Configuration
|
|
175
|
-
|
|
176
|
-
| Option | Type | Default | Description |
|
|
177
|
-
|--------|------|---------|-------------|
|
|
178
|
-
| `container` | `HTMLElement \| string` | — | **Required.** The DOM element (or CSS selector) to mount the editor into. |
|
|
179
|
-
| `value` | `string` | `''` | Initial markdown content. |
|
|
180
|
-
| `onChange` | `(value: string) => void` | — | Callback invoked every time the content changes (user input, `setValue()`, undo/redo). |
|
|
181
|
-
| `plugins` | `EditorPlugin[]` | all built-in plugins | Plugins to register. |
|
|
182
|
-
| `toolbar` | `ToolbarConfig` | default toolbar | Toolbar layout. |
|
|
183
|
-
| `theme` | `Record<string, string>` | — | CSS custom property overrides. |
|
|
184
|
-
| `mode` | `'split' \| 'editor' \| 'preview'` | `'split'` | Initial editor mode. |
|
|
185
|
-
| `placeholder` | `string` | — | Textarea placeholder text. |
|
|
186
|
-
| `readOnly` | `boolean` | `false` | Make the editor read-only. |
|
|
187
|
-
| `scrollSync` | `boolean` | `true` | Sync scroll position between editor and preview. |
|
|
188
|
-
| `renderers` | `Record<string, RendererFn>` | — | Custom renderer overrides. |
|
|
189
|
-
| `locale` | `Partial<EditorLocale>` | — | i18n overrides. |
|
|
432
|
+
---
|
|
190
433
|
|
|
191
434
|
## API Reference
|
|
192
435
|
|
|
193
436
|
```ts
|
|
194
|
-
interface
|
|
437
|
+
interface SynclineMDXEditor {
|
|
438
|
+
// Content
|
|
195
439
|
getValue(): string;
|
|
196
440
|
setValue(value: string): void;
|
|
441
|
+
|
|
442
|
+
// Editing
|
|
197
443
|
insertText(text: string): void;
|
|
444
|
+
insertBlock(template: string): void;
|
|
198
445
|
wrapSelection(prefix: string, suffix: string): void;
|
|
199
446
|
replaceSelection(text: string): void;
|
|
447
|
+
replaceCurrentLine(text: string): void;
|
|
448
|
+
insertAt(position: number, text: string): void;
|
|
449
|
+
|
|
450
|
+
// Selection & cursor
|
|
200
451
|
getSelection(): SelectionState;
|
|
201
|
-
|
|
452
|
+
setSelection(start: number, end: number): void;
|
|
453
|
+
getCurrentLine(): string;
|
|
454
|
+
getCurrentLineNumber(): number;
|
|
455
|
+
jumpToLine(lineNumber: number): void;
|
|
456
|
+
|
|
457
|
+
// Undo / Redo
|
|
458
|
+
undo(): void;
|
|
459
|
+
redo(): void;
|
|
460
|
+
|
|
461
|
+
// View
|
|
202
462
|
focus(): void;
|
|
203
463
|
renderPreview(): void;
|
|
204
|
-
getMode():
|
|
464
|
+
getMode(): 'split' | 'editor' | 'preview';
|
|
205
465
|
setMode(mode: 'split' | 'editor' | 'preview'): void;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
off(event: string, handler: Function): void;
|
|
210
|
-
undo(): void;
|
|
211
|
-
redo(): void;
|
|
466
|
+
setLineNumbers(enabled: boolean): void;
|
|
467
|
+
|
|
468
|
+
// Metrics
|
|
212
469
|
getWordCount(): number;
|
|
213
470
|
getLineCount(): number;
|
|
471
|
+
|
|
472
|
+
// DOM access
|
|
473
|
+
getRoot(): HTMLElement;
|
|
474
|
+
getTextarea(): HTMLTextAreaElement;
|
|
475
|
+
getPreview(): HTMLElement;
|
|
476
|
+
|
|
477
|
+
// Plugins
|
|
478
|
+
registerPlugin(plugin: EditorPlugin): void;
|
|
479
|
+
unregisterPlugin(name: string): void;
|
|
480
|
+
|
|
481
|
+
// Autocomplete
|
|
482
|
+
registerAutoComplete(items: CompletionItem | CompletionItem[]): void;
|
|
483
|
+
|
|
484
|
+
// Syntax highlighting
|
|
485
|
+
registerSyntaxHighlighter(fn: PluginTokenProvider | PluginTokenProvider[]): void;
|
|
486
|
+
|
|
487
|
+
// Theme sync
|
|
488
|
+
syncCodeEditorTheme(): void;
|
|
489
|
+
|
|
490
|
+
// Events
|
|
491
|
+
on(event: string, handler: (data?: unknown) => void): void;
|
|
492
|
+
off(event: string, handler: (data?: unknown) => void): void;
|
|
493
|
+
emit(event: string, data?: unknown): void;
|
|
494
|
+
|
|
495
|
+
// Lifecycle
|
|
214
496
|
destroy(): void;
|
|
215
497
|
}
|
|
216
498
|
```
|
|
217
499
|
|
|
218
|
-
|
|
500
|
+
---
|
|
219
501
|
|
|
220
|
-
|
|
502
|
+
## Events
|
|
221
503
|
|
|
222
|
-
|
|
504
|
+
### `onChange` callback (recommended)
|
|
223
505
|
|
|
224
|
-
```
|
|
506
|
+
```ts
|
|
225
507
|
const editor = createEditor({
|
|
226
508
|
container: '#editor',
|
|
227
509
|
onChange: (markdown) => {
|
|
@@ -232,81 +514,63 @@ const editor = createEditor({
|
|
|
232
514
|
|
|
233
515
|
### Event emitter
|
|
234
516
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
editor.on('change',
|
|
239
|
-
editor.on('
|
|
240
|
-
editor.on('
|
|
241
|
-
editor.on('
|
|
242
|
-
editor.on('toolbar-action',
|
|
517
|
+
```ts
|
|
518
|
+
editor.on('change', (markdown) => console.log('Changed:', markdown));
|
|
519
|
+
editor.on('selection-change', (sel) => console.log('Selection:', sel));
|
|
520
|
+
editor.on('mode-change', (mode) => console.log('Mode:', mode));
|
|
521
|
+
editor.on('render', (html) => console.log('Preview HTML ready'));
|
|
522
|
+
editor.on('focus', () => console.log('Editor focused'));
|
|
523
|
+
editor.on('blur', () => console.log('Editor blurred'));
|
|
524
|
+
editor.on('toolbar-action', (id) => console.log('Toolbar clicked:', id));
|
|
243
525
|
```
|
|
244
526
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
> **Note:** The GitHub repository for this package is **private** — the source code is not publicly visible. The package is freely installable from npm, but external contributions, forks, and pull requests are not accepted.
|
|
248
|
-
>
|
|
249
|
-
> To report a bug or request a feature, open an issue at the [GitHub Issues page](https://github.com/RadhaHariharan/syncline-mdx-editor/issues).
|
|
250
|
-
>
|
|
251
|
-
> ```bash
|
|
252
|
-
> npm install @synclineapi/mdx-editor
|
|
253
|
-
> ```
|
|
254
|
-
|
|
255
|
-
Internal development commands (for maintainers only):
|
|
256
|
-
|
|
257
|
-
```bash
|
|
258
|
-
cd syncline-mdx-editor
|
|
259
|
-
npm install
|
|
260
|
-
npm run dev # Start Vite dev server
|
|
261
|
-
npm run build # Build library for production
|
|
262
|
-
```
|
|
527
|
+
---
|
|
263
528
|
|
|
264
529
|
## Bundle Size
|
|
265
530
|
|
|
266
|
-
|
|
531
|
+
The production build targets **< 150 kB** (ESM + UMD, gzipped) when `mermaid`, `katex`, and `highlight.js` are treated as external peer dependencies.
|
|
267
532
|
|
|
268
533
|
```bash
|
|
269
534
|
npm run build
|
|
270
535
|
npm run size
|
|
271
536
|
```
|
|
272
537
|
|
|
273
|
-
|
|
538
|
+
---
|
|
274
539
|
|
|
275
540
|
## Security
|
|
276
541
|
|
|
277
|
-
All rendered Markdown/MDX HTML is sanitized
|
|
542
|
+
All rendered Markdown/MDX HTML is sanitized by a built-in XSS sanitizer before DOM injection. Dangerous tags (`script`, `iframe`, `object`), event-handler attributes (`onclick`, `onerror`, etc.), and `javascript:` protocol URLs are stripped automatically. See [SECURITY.md](./SECURITY.md) for the vulnerability disclosure policy.
|
|
543
|
+
|
|
544
|
+
---
|
|
278
545
|
|
|
279
546
|
## Accessibility
|
|
280
547
|
|
|
281
548
|
`@synclineapi/mdx-editor` targets **WCAG 2.1 AA** compliance:
|
|
282
|
-
|
|
549
|
+
|
|
550
|
+
- All toolbar buttons carry `aria-label` and `title` attributes
|
|
283
551
|
- The editor textarea has `aria-label="Markdown editor"` and `aria-multiline="true"`
|
|
284
552
|
- The preview pane has `role="region"`, `aria-label="Preview"`, and `aria-live="polite"`
|
|
285
553
|
- Mode toggle buttons expose `aria-pressed` state
|
|
286
|
-
- Toolbar dropdowns
|
|
287
|
-
- A skip link is provided for keyboard users
|
|
288
|
-
- All
|
|
554
|
+
- Toolbar dropdowns use `role="menu"` / `role="menuitem"`
|
|
555
|
+
- A skip link is provided for keyboard-only users
|
|
556
|
+
- All foreground/background colour pairs meet WCAG AA contrast ratios
|
|
289
557
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
```bash
|
|
293
|
-
npm run test # Run all tests
|
|
294
|
-
npm run test:watch # Watch mode
|
|
295
|
-
npm run test:coverage # Coverage report (target: 80%+)
|
|
296
|
-
npm run test:ui # Visual test UI
|
|
297
|
-
```
|
|
558
|
+
---
|
|
298
559
|
|
|
299
560
|
## Peer Dependencies
|
|
300
561
|
|
|
301
|
-
`mermaid`, `katex`, and `highlight.js` are optional
|
|
562
|
+
`mermaid`, `katex`, and `highlight.js` are optional. Install only the ones you need:
|
|
302
563
|
|
|
303
564
|
```bash
|
|
304
|
-
npm install mermaid #
|
|
305
|
-
npm install katex #
|
|
306
|
-
npm install highlight.js #
|
|
565
|
+
npm install mermaid # Mermaid diagrams
|
|
566
|
+
npm install katex # Math / formula rendering
|
|
567
|
+
npm install highlight.js # Syntax highlighting in code blocks
|
|
307
568
|
```
|
|
308
569
|
|
|
570
|
+
---
|
|
571
|
+
|
|
309
572
|
## License
|
|
310
573
|
|
|
311
|
-
|
|
574
|
+
Copyright © Syncline API. All rights reserved.
|
|
312
575
|
|
|
576
|
+
This software is proprietary and confidential. Unauthorized copying, distribution, modification, or use of this software, in whole or in part, is strictly prohibited without the express written permission of Syncline API.
|