@synclineapi/mdx-editor 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RadhaHariharan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,310 @@
1
+ # @synclineapi/mdx-editor
2
+
3
+ [![CI](https://github.com/RadhaHariharan/syncline-mdx-editor/actions/workflows/ci.yml/badge.svg)](https://github.com/RadhaHariharan/syncline-mdx-editor/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@synclineapi/mdx-editor.svg)](https://www.npmjs.com/package/@synclineapi/mdx-editor)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.4-blue.svg)](https://www.typescriptlang.org/)
7
+ [![WCAG AA](https://img.shields.io/badge/Accessibility-WCAG%20AA-green.svg)](https://www.w3.org/TR/WCAG21/)
8
+
9
+ A **framework-agnostic**, plugin-based MDX/Markdown editor with toolbar customization, live preview, and 33+ built-in features. Works with React, Vue, Angular, Next.js, Svelte, or plain JavaScript.
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ npm install @synclineapi/mdx-editor
15
+ ```
16
+
17
+ ### Vanilla JavaScript
18
+
19
+ ```html
20
+ <div id="editor"></div>
21
+
22
+ <script type="module">
23
+ import { createEditor } from '@synclineapi/mdx-editor';
24
+ import '@synclineapi/mdx-editor/style.css';
25
+
26
+ const editor = createEditor({
27
+ container: '#editor',
28
+ value: '# Hello World',
29
+ onChange: (markdown) => console.log('Content changed:', markdown),
30
+ });
31
+ </script>
32
+ ```
33
+
34
+ ### Custom Plugin Selection
35
+
36
+ ```js
37
+ import { SynclineMDXEditor, boldPlugin, italicPlugin, headingPlugin } from '@synclineapi/mdx-editor';
38
+ import '@synclineapi/mdx-editor/style.css';
39
+
40
+ const editor = new SynclineMDXEditor({
41
+ container: '#editor',
42
+ plugins: [headingPlugin(), boldPlugin(), italicPlugin()],
43
+ toolbar: [['heading', '|', 'bold', 'italic']],
44
+ });
45
+ ```
46
+
47
+ ## Features (33+ Built-in Plugins)
48
+
49
+ | Category | Plugins |
50
+ |----------|---------|
51
+ | **Formatting** | Heading (1–6), Bold, Italic, Strikethrough, Quote |
52
+ | **Links & Media** | Link, Image, Image Background, Image Frame |
53
+ | **Code** | Inline Code, Code Block (with syntax highlighting placeholder) |
54
+ | **Lists** | Unordered List, Ordered List, Task List |
55
+ | **Layout** | Table, Multi-Column (2–5), Tabs, Container |
56
+ | **Components** | Accordion, Accordion Group, Card, Card Group, Steps |
57
+ | **Callouts** | Admonition (Tip/Warning/Caution/Danger/Check/Info/Note), Tip (Good/Bad/Info) |
58
+ | **Rich Content** | Highlight (8 colors), Emoji, Formula (inline/block via KaTeX), Tooltip, Copy Text |
59
+ | **Embeds** | YouTube, Video file, GitHub Gist, Twitter/X, CodePen, CodeSandbox |
60
+ | **Diagrams** | Mermaid (Flowchart, Sequence, Class, State, ER, Journey, Gantt) |
61
+ | **Insert** | API Endpoint, Markdown Import, Code Snippet |
62
+
63
+ ## Architecture
64
+
65
+ ```
66
+ @synclineapi/mdx-editor
67
+ ├── Core Engine (SynclineMDXEditor)
68
+ │ ├── EventEmitter – Pub/sub event system
69
+ │ ├── PluginManager – Plugin lifecycle
70
+ │ ├── Toolbar – Configurable toolbar renderer
71
+ │ ├── Renderer – Markdown/MDX → HTML engine
72
+ │ └── History – Undo/redo stack
73
+ └── Plugins (33+)
74
+ └── Each exports: toolbarItems, shortcuts, renderers, parsers, styles
75
+ ```
76
+
77
+ ## Plugin API
78
+
79
+ Create custom plugins:
80
+
81
+ ```ts
82
+ import type { EditorPlugin } from '@synclineapi/mdx-editor';
83
+
84
+ const myPlugin: EditorPlugin = {
85
+ name: 'my-custom-block',
86
+ toolbarItems: [{
87
+ id: 'myBlock',
88
+ label: 'My Block',
89
+ icon: '<svg>...</svg>',
90
+ tooltip: 'Insert my block',
91
+ action: ({ editor }) => {
92
+ editor.insertBlock('<MyBlock>\n Content\n</MyBlock>');
93
+ },
94
+ }],
95
+ renderers: [{
96
+ name: 'myBlock',
97
+ pattern: /<MyBlock>([\s\S]*?)<\/MyBlock>/g,
98
+ render: (match) => {
99
+ const m = match.match(/<MyBlock>([\s\S]*?)<\/MyBlock>/);
100
+ return `<div class="my-block">${m?.[1] || ''}</div>`;
101
+ },
102
+ }],
103
+ shortcuts: [
104
+ { key: 'Ctrl+Shift+m', action: ({ editor }) => editor.insertBlock('<MyBlock>\n Content\n</MyBlock>') },
105
+ ],
106
+ styles: `.my-block { border: 2px solid blue; padding: 16px; border-radius: 8px; }`,
107
+ };
108
+ ```
109
+
110
+ ## Toolbar Customization
111
+
112
+ ### Flat toolbar
113
+
114
+ ```js
115
+ createEditor({
116
+ container: '#editor',
117
+ toolbar: [['bold', 'italic', '|', 'heading', '|', 'link', 'image']],
118
+ });
119
+ ```
120
+
121
+ ### Nested dropdowns
122
+
123
+ ```js
124
+ createEditor({
125
+ container: '#editor',
126
+ toolbar: [[
127
+ 'bold', 'italic', '|',
128
+ {
129
+ type: 'group',
130
+ label: 'Insert',
131
+ display: 'dropdown',
132
+ items: ['table', 'accordion', 'card', 'steps'],
133
+ },
134
+ ]],
135
+ });
136
+ ```
137
+
138
+ ## Theming
139
+
140
+ Override CSS custom properties:
141
+
142
+ ```css
143
+ .smdx-editor {
144
+ --smdx-primary: #e11d48;
145
+ --smdx-bg: #0f172a;
146
+ --smdx-text: #e2e8f0;
147
+ --smdx-border: #334155;
148
+ --smdx-font-family: 'Inter', sans-serif;
149
+ --smdx-font-mono: 'JetBrains Mono', monospace;
150
+ --smdx-radius: 12px;
151
+ }
152
+ ```
153
+
154
+ Or pass theme via config:
155
+
156
+ ```js
157
+ createEditor({
158
+ container: '#editor',
159
+ theme: {
160
+ '--smdx-primary': '#e11d48',
161
+ '--smdx-bg': '#0f172a',
162
+ },
163
+ });
164
+ ```
165
+
166
+ Add `.smdx-dark` class for dark mode:
167
+
168
+ ```js
169
+ editor.getRoot().classList.add('smdx-dark');
170
+ ```
171
+
172
+ ## Configuration
173
+
174
+ | Option | Type | Default | Description |
175
+ |--------|------|---------|-------------|
176
+ | `container` | `HTMLElement \| string` | — | **Required.** The DOM element (or CSS selector) to mount the editor into. |
177
+ | `value` | `string` | `''` | Initial markdown content. |
178
+ | `onChange` | `(value: string) => void` | — | Callback invoked every time the content changes (user input, `setValue()`, undo/redo). |
179
+ | `plugins` | `EditorPlugin[]` | all built-in plugins | Plugins to register. |
180
+ | `toolbar` | `ToolbarConfig` | default toolbar | Toolbar layout. |
181
+ | `theme` | `Record<string, string>` | — | CSS custom property overrides. |
182
+ | `mode` | `'split' \| 'editor' \| 'preview'` | `'split'` | Initial editor mode. |
183
+ | `placeholder` | `string` | — | Textarea placeholder text. |
184
+ | `readOnly` | `boolean` | `false` | Make the editor read-only. |
185
+ | `scrollSync` | `boolean` | `true` | Sync scroll position between editor and preview. |
186
+ | `renderers` | `Record<string, RendererFn>` | — | Custom renderer overrides. |
187
+ | `locale` | `Partial<EditorLocale>` | — | i18n overrides. |
188
+
189
+ ## API Reference
190
+
191
+ ```ts
192
+ interface EditorAPI {
193
+ getValue(): string;
194
+ setValue(value: string): void;
195
+ insertText(text: string): void;
196
+ wrapSelection(prefix: string, suffix: string): void;
197
+ replaceSelection(text: string): void;
198
+ getSelection(): SelectionState;
199
+ insertBlock(template: string): void;
200
+ focus(): void;
201
+ renderPreview(): void;
202
+ getMode(): EditorMode;
203
+ setMode(mode: 'split' | 'editor' | 'preview'): void;
204
+ registerPlugin(plugin: EditorPlugin): void;
205
+ unregisterPlugin(name: string): void;
206
+ on(event: string, handler: Function): void;
207
+ off(event: string, handler: Function): void;
208
+ undo(): void;
209
+ redo(): void;
210
+ getWordCount(): number;
211
+ getLineCount(): number;
212
+ destroy(): void;
213
+ }
214
+ ```
215
+
216
+ ## Events
217
+
218
+ ### `onChange` config callback (recommended)
219
+
220
+ Pass `onChange` directly in the config for the simplest way to listen for content changes:
221
+
222
+ ```js
223
+ const editor = createEditor({
224
+ container: '#editor',
225
+ onChange: (markdown) => {
226
+ console.log('Content changed:', markdown);
227
+ },
228
+ });
229
+ ```
230
+
231
+ ### Event emitter
232
+
233
+ For all events (including selection changes, mode changes, etc.) use the event emitter API:
234
+
235
+ ```js
236
+ editor.on('change', (markdown) => console.log('Content changed:', markdown));
237
+ editor.on('selection-change', (sel) => console.log('Selection:', sel));
238
+ editor.on('mode-change', (mode) => console.log('Mode:', mode));
239
+ editor.on('render', (html) => console.log('Preview rendered'));
240
+ editor.on('toolbar-action', (id) => console.log('Toolbar clicked:', id));
241
+ ```
242
+
243
+ ## Development
244
+
245
+ > **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.
246
+ >
247
+ > To report a bug or request a feature, open an issue at the [GitHub Issues page](https://github.com/RadhaHariharan/syncline-mdx-editor/issues).
248
+ >
249
+ > ```bash
250
+ > npm install @synclineapi/mdx-editor
251
+ > ```
252
+
253
+ Internal development commands (for maintainers only):
254
+
255
+ ```bash
256
+ cd syncline-mdx-editor
257
+ npm install
258
+ npm run dev # Start Vite dev server
259
+ npm run build # Build library for production
260
+ ```
261
+
262
+ ## Bundle Size
263
+
264
+ Check the estimated npm publish size before publishing:
265
+
266
+ ```bash
267
+ npm run build
268
+ npm run size
269
+ ```
270
+
271
+ The core bundle (with `mermaid`, `katex`, `highlight.js` externalised) targets **< 150 kB** for both ESM and UMD outputs.
272
+
273
+ ## Security
274
+
275
+ All rendered Markdown/MDX HTML is sanitized via a built-in XSS sanitizer before DOM injection. Dangerous tags (`script`, `iframe`, `object`), event handler attributes (`onclick`, `onerror`, etc.), and `javascript:` protocol URLs are all stripped. See [SECURITY.md](./SECURITY.md) for the vulnerability disclosure policy.
276
+
277
+ ## Accessibility
278
+
279
+ `@synclineapi/mdx-editor` targets **WCAG 2.1 AA** compliance:
280
+ - All toolbar buttons have `aria-label` and `title` attributes
281
+ - The editor textarea has `aria-label="Markdown editor"` and `aria-multiline="true"`
282
+ - The preview pane has `role="region"`, `aria-label="Preview"`, and `aria-live="polite"`
283
+ - Mode toggle buttons expose `aria-pressed` state
284
+ - Toolbar dropdowns have `role="menu"` / `role="menuitem"`
285
+ - A skip link is provided for keyboard users
286
+ - All text meets WCAG AA color contrast ratios
287
+
288
+ ## Testing
289
+
290
+ ```bash
291
+ npm run test # Run all tests
292
+ npm run test:watch # Watch mode
293
+ npm run test:coverage # Coverage report (target: 80%+)
294
+ npm run test:ui # Visual test UI
295
+ ```
296
+
297
+ ## Peer Dependencies
298
+
299
+ `mermaid`, `katex`, and `highlight.js` are optional peer dependencies. Install them only if you use those features:
300
+
301
+ ```bash
302
+ npm install mermaid # For Mermaid diagrams
303
+ npm install katex # For math formulas
304
+ npm install highlight.js # Included by default for syntax highlighting
305
+ ```
306
+
307
+ ## License
308
+
309
+ MIT
310
+
@@ -0,0 +1,61 @@
1
+ import { EditorConfig, EditorAPI, EditorPlugin, EditorMode, SelectionState } from './types';
2
+ export declare class SynclineMDXEditor implements EditorAPI {
3
+ private root;
4
+ private toolbarEl;
5
+ private editorPane;
6
+ private previewPane;
7
+ private textarea;
8
+ private previewContent;
9
+ private statusBar;
10
+ private events;
11
+ private pluginManager;
12
+ private toolbar;
13
+ private renderer;
14
+ private history;
15
+ private mode;
16
+ private config;
17
+ private renderTimer;
18
+ private historyTimer;
19
+ private _destroyed;
20
+ constructor(config: EditorConfig);
21
+ private init;
22
+ private buildDOM;
23
+ private registerPlugins;
24
+ private getDefaultToolbar;
25
+ private onInput;
26
+ private onKeyDown;
27
+ private matchShortcut;
28
+ private onSelectionChange;
29
+ private scheduleRender;
30
+ private updateStatusBar;
31
+ getValue(): string;
32
+ setValue(value: string): void;
33
+ insertText(text: string): void;
34
+ wrapSelection(prefix: string, suffix: string): void;
35
+ replaceSelection(text: string): void;
36
+ getSelection(): SelectionState;
37
+ setSelection(start: number, end: number): void;
38
+ insertBlock(template: string): void;
39
+ getTextarea(): HTMLTextAreaElement;
40
+ getPreview(): HTMLElement;
41
+ getRoot(): HTMLElement;
42
+ focus(): void;
43
+ renderPreview(): Promise<void>;
44
+ getMode(): EditorMode;
45
+ setMode(mode: EditorMode): void;
46
+ registerPlugin(plugin: EditorPlugin): void;
47
+ unregisterPlugin(name: string): void;
48
+ on(event: string, handler: (data?: unknown) => void): void;
49
+ off(event: string, handler: (data?: unknown) => void): void;
50
+ emit(event: string, data?: unknown): void;
51
+ destroy(): void;
52
+ undo(): void;
53
+ redo(): void;
54
+ getCurrentLine(): string;
55
+ getCurrentLineNumber(): number;
56
+ replaceCurrentLine(text: string): void;
57
+ insertAt(position: number, text: string): void;
58
+ getWordCount(): number;
59
+ getLineCount(): number;
60
+ private applyTheme;
61
+ }
@@ -0,0 +1,8 @@
1
+ import { EventHandler } from './types';
2
+ export declare class EventEmitter {
3
+ private listeners;
4
+ on(event: string, handler: EventHandler): void;
5
+ off(event: string, handler: EventHandler): void;
6
+ emit(event: string, data?: unknown): void;
7
+ removeAllListeners(event?: string): void;
8
+ }
@@ -0,0 +1,12 @@
1
+ /** Simple undo/redo history stack */
2
+ export declare class History {
3
+ private stack;
4
+ private pointer;
5
+ private maxSize;
6
+ constructor(maxSize?: number);
7
+ push(value: string): void;
8
+ undo(): string | undefined;
9
+ redo(): string | undefined;
10
+ current(): string | undefined;
11
+ clear(): void;
12
+ }
@@ -0,0 +1,2 @@
1
+ /** Central SVG icon registry for toolbar buttons */
2
+ export declare const icons: Record<string, string>;
@@ -0,0 +1,24 @@
1
+ import { EditorPlugin, ToolbarItemConfig, ShortcutConfig, RendererConfig, ParserConfig, EditorAPI } from './types';
2
+ import { EventEmitter } from './events';
3
+ export declare class PluginManager {
4
+ private editorApi;
5
+ private plugins;
6
+ private toolbarItems;
7
+ private shortcuts;
8
+ private renderers;
9
+ private parsers;
10
+ private styleElements;
11
+ private events;
12
+ constructor(editorApi: EditorAPI, events: EventEmitter);
13
+ register(plugin: EditorPlugin): Promise<void>;
14
+ unregister(name: string): void;
15
+ getToolbarItem(id: string): ToolbarItemConfig | undefined;
16
+ getAllToolbarItems(): Map<string, ToolbarItemConfig>;
17
+ getShortcuts(): ShortcutConfig[];
18
+ getRenderers(): RendererConfig[];
19
+ getParsers(): ParserConfig[];
20
+ hasPlugin(name: string): boolean;
21
+ private createContext;
22
+ private injectStyles;
23
+ destroy(): void;
24
+ }
@@ -0,0 +1,28 @@
1
+ import { RendererConfig, ParserConfig } from './types';
2
+ /**
3
+ * The Renderer transforms markdown+MDX source into preview HTML.
4
+ * It runs custom parsers first, then applies marked for standard markdown,
5
+ * then runs custom renderers for extended blocks.
6
+ */
7
+ export declare class Renderer {
8
+ private renderers;
9
+ private parsers;
10
+ setRenderers(renderers: RendererConfig[]): void;
11
+ setParsers(parsers: ParserConfig[]): void;
12
+ render(source: string): Promise<string>;
13
+ private applyParsers;
14
+ private applyRenderers;
15
+ /**
16
+ * Lightweight built-in markdown renderer.
17
+ * Handles standard markdown without external deps.
18
+ */
19
+ private renderMarkdown;
20
+ private renderTables;
21
+ private parseTableRow;
22
+ private escapeHtml;
23
+ /**
24
+ * Validate MDX component tags — detect unclosed or mismatched tags.
25
+ * Only checks PascalCase tags (MDX components), not standard HTML.
26
+ */
27
+ private validateMdxTags;
28
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Lightweight HTML sanitizer to prevent XSS.
3
+ * Strips dangerous tags (script, object, embed, style, base, form, input, link, meta)
4
+ * and dangerous attributes (on*, href=javascript:, src=javascript:, action, formaction).
5
+ * Iframes are allowed only when their src points to a known safe embed origin.
6
+ * Buttons are allowed (needed for code-block copy functionality); on* attributes are
7
+ * still stripped from them so no inline scripts can execute.
8
+ * Uses the browser's DOMParser for correctness, with a fallback regex pass for SSR/test environments.
9
+ */
10
+ export declare function sanitizeHtml(html: string): string;
@@ -0,0 +1,29 @@
1
+ import { ToolbarConfig, EditorAPI } from './types';
2
+ import { PluginManager } from './plugin-manager';
3
+ import { EventEmitter } from './events';
4
+ export declare class Toolbar {
5
+ private config;
6
+ private pluginManager;
7
+ private editorApi;
8
+ private events;
9
+ private el;
10
+ private activeDropdown;
11
+ private documentClickHandler;
12
+ constructor(config: ToolbarConfig, pluginManager: PluginManager, editorApi: EditorAPI, events: EventEmitter);
13
+ render(container: HTMLElement): HTMLElement;
14
+ private renderRow;
15
+ private renderDivider;
16
+ private renderItem;
17
+ private renderGroup;
18
+ private renderDropdownGroup;
19
+ private renderDropdownItem;
20
+ private renderSubmenu;
21
+ private createButton;
22
+ private createMenuButton;
23
+ private executeAction;
24
+ private getActionContext;
25
+ private toggleDropdown;
26
+ private closeDropdowns;
27
+ updateActiveStates(): void;
28
+ destroy(): void;
29
+ }