@remyxjs/core 1.0.0-beta

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.
Files changed (105) hide show
  1. package/README.md +2454 -0
  2. package/dist/convertCsv-B8RVtdcs.cjs +2 -0
  3. package/dist/convertCsv-B8RVtdcs.cjs.map +1 -0
  4. package/dist/convertCsv-CKzZjzLJ.js +2 -0
  5. package/dist/convertCsv-CKzZjzLJ.js.map +1 -0
  6. package/dist/convertDocx-4q89XLLv.cjs +2 -0
  7. package/dist/convertDocx-4q89XLLv.cjs.map +1 -0
  8. package/dist/convertDocx-Dmx88twM.js +2 -0
  9. package/dist/convertDocx-Dmx88twM.js.map +1 -0
  10. package/dist/convertHtml-CtYVhiTh.js +2 -0
  11. package/dist/convertHtml-CtYVhiTh.js.map +1 -0
  12. package/dist/convertHtml-DbHrdrD3.cjs +2 -0
  13. package/dist/convertHtml-DbHrdrD3.cjs.map +1 -0
  14. package/dist/convertMarkdown-Di239Gtn.js +2 -0
  15. package/dist/convertMarkdown-Di239Gtn.js.map +1 -0
  16. package/dist/convertMarkdown-eJ9Nkoid.cjs +2 -0
  17. package/dist/convertMarkdown-eJ9Nkoid.cjs.map +1 -0
  18. package/dist/convertPdf-CFA1eNNH.js +2 -0
  19. package/dist/convertPdf-CFA1eNNH.js.map +1 -0
  20. package/dist/convertPdf-CSLmTrB8.cjs +2 -0
  21. package/dist/convertPdf-CSLmTrB8.cjs.map +1 -0
  22. package/dist/convertRtf-08CoScGD.js +2 -0
  23. package/dist/convertRtf-08CoScGD.js.map +1 -0
  24. package/dist/convertRtf-BfiBLMig.cjs +2 -0
  25. package/dist/convertRtf-BfiBLMig.cjs.map +1 -0
  26. package/dist/convertText-BpgzHRuh.cjs +2 -0
  27. package/dist/convertText-BpgzHRuh.cjs.map +1 -0
  28. package/dist/convertText-sa7PxKTe.js +2 -0
  29. package/dist/convertText-sa7PxKTe.js.map +1 -0
  30. package/dist/index-4syk9eEO.js +2 -0
  31. package/dist/index-4syk9eEO.js.map +1 -0
  32. package/dist/index-B25zSs0W.js +2 -0
  33. package/dist/index-B25zSs0W.js.map +1 -0
  34. package/dist/index-B7VT6ZLa.cjs +2 -0
  35. package/dist/index-B7VT6ZLa.cjs.map +1 -0
  36. package/dist/index-BCpytFKJ.js +2 -0
  37. package/dist/index-BCpytFKJ.js.map +1 -0
  38. package/dist/index-BNKANY5i.cjs +2 -0
  39. package/dist/index-BNKANY5i.cjs.map +1 -0
  40. package/dist/index-B_g_579T.cjs +2 -0
  41. package/dist/index-B_g_579T.cjs.map +1 -0
  42. package/dist/index-BvwyeoMb.js +3 -0
  43. package/dist/index-BvwyeoMb.js.map +1 -0
  44. package/dist/index-Bw7mlUQo.js +2 -0
  45. package/dist/index-Bw7mlUQo.js.map +1 -0
  46. package/dist/index-Byatzd-A.js +2 -0
  47. package/dist/index-Byatzd-A.js.map +1 -0
  48. package/dist/index-C0z9eZLm.cjs +2 -0
  49. package/dist/index-C0z9eZLm.cjs.map +1 -0
  50. package/dist/index-C88XPqjX.js +2 -0
  51. package/dist/index-C88XPqjX.js.map +1 -0
  52. package/dist/index-CI6FPF49.cjs +2 -0
  53. package/dist/index-CI6FPF49.cjs.map +1 -0
  54. package/dist/index-CLZF5_GB.cjs +2 -0
  55. package/dist/index-CLZF5_GB.cjs.map +1 -0
  56. package/dist/index-CXSwYlG4.cjs +2 -0
  57. package/dist/index-CXSwYlG4.cjs.map +1 -0
  58. package/dist/index-Ch9gotLk.js +2 -0
  59. package/dist/index-Ch9gotLk.js.map +1 -0
  60. package/dist/index-CifDpN1Y.js +2 -0
  61. package/dist/index-CifDpN1Y.js.map +1 -0
  62. package/dist/index-D5o8VpWJ.cjs +2 -0
  63. package/dist/index-D5o8VpWJ.cjs.map +1 -0
  64. package/dist/index-DKT1bABL.js +2 -0
  65. package/dist/index-DKT1bABL.js.map +1 -0
  66. package/dist/index-DWcn72PW.js +2 -0
  67. package/dist/index-DWcn72PW.js.map +1 -0
  68. package/dist/index-DjCGzPEv.cjs +2 -0
  69. package/dist/index-DjCGzPEv.cjs.map +1 -0
  70. package/dist/index-Dq0Jr1Ae.js +2 -0
  71. package/dist/index-Dq0Jr1Ae.js.map +1 -0
  72. package/dist/index-Dw0MVypb.cjs +2 -0
  73. package/dist/index-Dw0MVypb.cjs.map +1 -0
  74. package/dist/index-FEo3LShh.cjs +2 -0
  75. package/dist/index-FEo3LShh.cjs.map +1 -0
  76. package/dist/index-O1hzAUzi.cjs +2 -0
  77. package/dist/index-O1hzAUzi.cjs.map +1 -0
  78. package/dist/index-T1ZyLzeF.cjs +2 -0
  79. package/dist/index-T1ZyLzeF.cjs.map +1 -0
  80. package/dist/index-iRikoCdK.cjs +2 -0
  81. package/dist/index-iRikoCdK.cjs.map +1 -0
  82. package/dist/index-l6Yddj6x.js +2 -0
  83. package/dist/index-l6Yddj6x.js.map +1 -0
  84. package/dist/index-rD8LZENp.js +2 -0
  85. package/dist/index-rD8LZENp.js.map +1 -0
  86. package/dist/remyx-core.cjs +2 -0
  87. package/dist/remyx-core.cjs.map +1 -0
  88. package/dist/remyx-core.css +1 -0
  89. package/dist/remyx-core.js +2 -0
  90. package/dist/remyx-core.js.map +1 -0
  91. package/dist/themes/callouts.css +79 -0
  92. package/dist/themes/collaboration.css +117 -0
  93. package/dist/themes/comments.css +198 -0
  94. package/dist/themes/dark.css +109 -0
  95. package/dist/themes/forest.css +109 -0
  96. package/dist/themes/light.css +4 -0
  97. package/dist/themes/links.css +115 -0
  98. package/dist/themes/math-toc-analytics.css +129 -0
  99. package/dist/themes/ocean.css +109 -0
  100. package/dist/themes/rose.css +109 -0
  101. package/dist/themes/spellcheck.css +173 -0
  102. package/dist/themes/sunset.css +109 -0
  103. package/dist/themes/templates.css +87 -0
  104. package/dist/themes/variables.css +3222 -0
  105. package/package.json +80 -0
package/README.md ADDED
@@ -0,0 +1,2454 @@
1
+ ![Remyx Editor](../docs/screenshots/Remyx-Logo.svg)
2
+
3
+ # @remyxjs/core
4
+
5
+ Framework-agnostic core engine for the Remyx Editor. Provides the editor engine, commands, plugin system, utilities, and CSS themes — with zero framework dependencies.
6
+
7
+ Use this package to build Remyx Editor integrations for any framework (Vue, Svelte, Angular, vanilla JS) or for server-side processing. For React projects, use [`@remyxjs/react`](../remyx-react/), which includes this package plus React components and hooks.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Installation](#installation)
12
+ - [Quick Start](#quick-start)
13
+ - [Architecture](#architecture)
14
+ - [EditorEngine](#editorengine)
15
+ - [Constructor Options](#constructor-options)
16
+ - [Methods](#methods)
17
+ - [Events](#events)
18
+ - [Commands](#commands)
19
+ - [Formatting](#formatting)
20
+ - [Headings](#headings)
21
+ - [Lists](#lists)
22
+ - [Alignment](#alignment)
23
+ - [Links](#links)
24
+ - [Images](#images)
25
+ - [Tables](#tables)
26
+ - [Blocks](#blocks)
27
+ - [Fonts](#fonts)
28
+ - [Media Embeds](#media-embeds)
29
+ - [Find & Replace](#find--replace)
30
+ - [Source Mode](#source-mode)
31
+ - [Fullscreen](#fullscreen)
32
+ - [Distraction-Free Mode](#distraction-free-mode)
33
+ - [Split View](#split-view)
34
+ - [Color Presets](#color-presets)
35
+ - [Typography Controls](#typography-controls)
36
+ - [Sticky Toolbar](#sticky-toolbar)
37
+ - [Markdown Toggle](#markdown-toggle)
38
+ - [Attachments](#attachments)
39
+ - [Document Import](#document-import)
40
+ - [Plugin System](#plugin-system)
41
+ - [Creating Plugins](#creating-plugins)
42
+ - [Plugin API (Restricted)](#plugin-api-restricted)
43
+ - [Built-in Plugins](#built-in-plugins)
44
+ - [Syntax Highlighting](#syntax-highlighting)
45
+ - [Autosave](#autosave)
46
+ - [Storage Providers](#storage-providers)
47
+ - [AutosaveManager API](#autosavemanager-api)
48
+ - [Autosave Events](#autosave-events)
49
+ - [Selection API](#selection-api)
50
+ - [History (Undo/Redo)](#history-undoredo)
51
+ - [Keyboard Shortcuts](#keyboard-shortcuts)
52
+ - [Sanitizer](#sanitizer)
53
+ - [Utilities](#utilities)
54
+ - [Markdown Conversion](#markdown-conversion)
55
+ - [Document Conversion](#document-conversion)
56
+ - [Export](#export)
57
+ - [Paste Cleaning](#paste-cleaning)
58
+ - [Font Management](#font-management)
59
+ - [DOM Utilities](#dom-utilities)
60
+ - [HTML Formatting](#html-formatting)
61
+ - [Platform Detection](#platform-detection)
62
+ - [Theming](#theming)
63
+ - [Theme Variables](#theme-variables)
64
+ - [Theme Presets](#theme-presets)
65
+ - [Custom Themes](#custom-themes)
66
+ - [Toolbar Configuration](#toolbar-configuration)
67
+ - [Toolbar Presets](#toolbar-presets)
68
+ - [Custom Toolbars](#custom-toolbars)
69
+ - [Toolbar Item Theming](#toolbar-item-theming)
70
+ - [Configuration](#configuration)
71
+ - [defineConfig](#defineconfig)
72
+ - [Multi-Editor Support](#multi-editor-support)
73
+ - [EditorBus](#editorbus)
74
+ - [SharedResources](#sharedresources)
75
+ - [Constants](#constants)
76
+ - [Tree-Shaking](#tree-shaking)
77
+ - [CSS](#css)
78
+ - [Building Framework Wrappers](#building-framework-wrappers)
79
+ - [License](#license)
80
+
81
+ ## Installation
82
+
83
+ ```bash
84
+ npm install @remyxjs/core
85
+ ```
86
+
87
+ ## Quick Start
88
+
89
+ ```js
90
+ import { EditorEngine } from '@remyxjs/core';
91
+ import {
92
+ registerFormattingCommands,
93
+ registerHeadingCommands,
94
+ registerListCommands,
95
+ registerLinkCommands,
96
+ registerImageCommands,
97
+ registerTableCommands,
98
+ registerBlockCommands,
99
+ } from '@remyxjs/core';
100
+ import '@remyxjs/core/style.css';
101
+
102
+ const element = document.querySelector('#editor');
103
+ const engine = new EditorEngine(element, { outputFormat: 'html' });
104
+
105
+ // Register the commands you need
106
+ registerFormattingCommands(engine);
107
+ registerHeadingCommands(engine);
108
+ registerListCommands(engine);
109
+ registerLinkCommands(engine);
110
+ registerImageCommands(engine);
111
+ registerTableCommands(engine);
112
+ registerBlockCommands(engine);
113
+
114
+ // Initialize
115
+ engine.init();
116
+
117
+ // Listen for changes
118
+ engine.on('content:change', () => {
119
+ console.log(engine.getHTML());
120
+ });
121
+
122
+ // Execute commands
123
+ engine.executeCommand('bold');
124
+ engine.executeCommand('heading', 2);
125
+
126
+ // Cleanup when done
127
+ engine.destroy();
128
+ ```
129
+
130
+ ## Architecture
131
+
132
+ ```
133
+ @remyxjs/core
134
+ core/ EditorEngine, EventBus, CommandRegistry, Selection,
135
+ History, KeyboardManager, Sanitizer, Clipboard, DragDrop,
136
+ AutosaveManager, EditorBus.js, SharedResources.js, VirtualScroller.js
137
+ commands/ 20 register functions (formatting, headings, lists, slashCommands, etc.)
138
+ plugins/ PluginManager, createPlugin, 17 built-in plugins
139
+ workers/ WorkerPool for background thread offloading
140
+ autosave/ 5 storage providers (LocalStorage, SessionStorage, FileSystem, Cloud, Custom)
141
+ i18n/ Translations and locale support
142
+ utils/ markdown, paste cleaning, export, fonts, themes, toolbar, DOM,
143
+ documentConverter/ (per-format modules), escapeHTML.js,
144
+ insertPlainText.js, rtl.js, performance.js
145
+ constants/ defaults, keybindings, schema, commands
146
+ config/ defineConfig, loadConfig.js
147
+ themes/ variables.css, light.css, dark.css, ocean.css, forest.css, sunset.css, rose.css
148
+ ```
149
+
150
+ ## EditorEngine
151
+
152
+ The central class. Takes a DOM element and manages all contenteditable editing, event handling, commands, history, and plugins.
153
+
154
+ ### Constructor Options
155
+
156
+ ```js
157
+ const engine = new EditorEngine(element, {
158
+ outputFormat: 'html', // 'html' or 'markdown'
159
+ history: {
160
+ maxSize: 100, // Maximum undo states
161
+ debounceMs: 300, // Debounce interval for snapshots
162
+ },
163
+ sanitize: {
164
+ allowedTags: { ... }, // Tag-to-attributes map (extends defaults)
165
+ allowedStyles: [ ... ], // Allowed CSS properties (extends defaults)
166
+ },
167
+ baseHeadingLevel: 1, // Heading offset (2 renders H1 as <h2>)
168
+ uploadHandler: async (file) => {
169
+ // Return a URL string for the uploaded file
170
+ const url = await myUploadService(file);
171
+ return url;
172
+ },
173
+ maxFileSize: 10 * 1024 * 1024, // 10 MB (default)
174
+ });
175
+ ```
176
+
177
+ ### Methods
178
+
179
+ | Method | Returns | Description |
180
+ | --- | --- | --- |
181
+ | `init()` | `void` | Initialize the editor — binds event listeners, starts subsystems |
182
+ | `destroy()` | `void` | Clean up all listeners, disconnect observers, destroy plugins |
183
+ | `getHTML()` | `string` | Get sanitized HTML content |
184
+ | `setHTML(html)` | `void` | Set content (sanitized before insertion) |
185
+ | `getText()` | `string` | Get plain text content |
186
+ | `isEmpty()` | `boolean` | `true` when editor has no meaningful content |
187
+ | `focus()` | `void` | Focus the editor element |
188
+ | `blur()` | `void` | Blur the editor element |
189
+ | `executeCommand(name, ...args)` | `any` | Execute a registered command by name |
190
+ | `on(event, handler)` | `Function` | Subscribe to an event; returns an unsubscribe function |
191
+ | `off(event, handler)` | `void` | Unsubscribe from an event |
192
+ | `getWordCount()` | `number` | Current word count |
193
+ | `getCharCount()` | `number` | Current character count |
194
+
195
+ ### Events
196
+
197
+ | Event | Data | Description |
198
+ | --- | --- | --- |
199
+ | `content:change` | — | Content was modified |
200
+ | `selection:change` | `ActiveFormats` | Selection or formatting state changed |
201
+ | `focus` | — | Editor received focus |
202
+ | `blur` | — | Editor lost focus |
203
+ | `command:executed` | `{ name, args, result }` | A command was executed |
204
+ | `paste` | `{ html, text }` | Paste occurred |
205
+ | `drop` | `{ files, html }` | Drop occurred |
206
+ | `upload:error` | `{ file, error }` | Upload handler rejected |
207
+ | `file:too-large` | `{ file, maxSize }` | Dropped/pasted file exceeded size limit |
208
+ | `editor:error` | `{ phase, error }` | Initialization error |
209
+ | `mode:change` | `{ sourceMode }` | Source mode toggled |
210
+ | `mode:change:markdown` | `{ markdownMode }` | Markdown mode toggled |
211
+ | `fullscreen:toggle` | `{ fullscreen }` | Fullscreen toggled |
212
+ | `find:results` | `{ total, current }` | Find/replace results updated |
213
+ | `history:undo` | — | Undo performed |
214
+ | `history:redo` | — | Redo performed |
215
+ | `plugin:registered` | `{ name }` | Plugin was registered |
216
+ | `plugin:error` | `{ name, error }` | Plugin init/destroy error |
217
+ | `codeblock:created` | `{ element, language }` | Code block was created |
218
+ | `codeblock:language-change` | `{ language, element }` | Code block language was changed |
219
+ | `wordcount:update` | `{ wordCount, charCount }` | Word/char count changed |
220
+ | `autosave:saving` | — | Autosave started |
221
+ | `autosave:saved` | `{ timestamp }` | Autosave succeeded |
222
+ | `autosave:error` | `{ error }` | Autosave failed |
223
+ | `autosave:recovered` | `{ recoveredContent, timestamp }` | Recovery data found on init |
224
+
225
+ **Example — listening to events:**
226
+
227
+ ```js
228
+ engine.on('content:change', () => {
229
+ saveToServer(engine.getHTML());
230
+ });
231
+
232
+ engine.on('selection:change', (formats) => {
233
+ updateToolbarState(formats);
234
+ // formats: { bold, italic, underline, heading, alignment, link, ... }
235
+ });
236
+
237
+ engine.on('upload:error', ({ file, error }) => {
238
+ showNotification(`Upload failed: ${error.message}`);
239
+ });
240
+
241
+ // The return value is an unsubscribe function
242
+ const unsub = engine.on('focus', () => console.log('focused'));
243
+ unsub(); // stop listening
244
+ ```
245
+
246
+ ## Commands
247
+
248
+ Each register function adds commands to the engine. Call only the ones you need — unused commands are tree-shaken from the bundle.
249
+
250
+ | Function | Commands Added |
251
+ | --- | --- |
252
+ | `registerFormattingCommands` | bold, italic, underline, strikethrough, subscript, superscript, removeFormat |
253
+ | `registerHeadingCommands` | heading, h1–h6, paragraph |
254
+ | `registerAlignmentCommands` | alignLeft, alignCenter, alignRight, alignJustify |
255
+ | `registerListCommands` | orderedList, unorderedList, taskList, indent, outdent |
256
+ | `registerLinkCommands` | insertLink, editLink, removeLink |
257
+ | `registerImageCommands` | insertImage, resizeImage, alignImage, removeImage |
258
+ | `registerTableCommands` | insertTable, addRowBefore, addRowAfter, addColBefore, addColAfter, deleteRow, deleteCol, deleteTable, mergeCells, splitCell, toggleHeaderRow, sortTable, filterTable, clearTableFilters, formatCell, evaluateFormulas |
259
+ | `registerBlockCommands` | blockquote, codeBlock, horizontalRule |
260
+ | `registerFontCommands` | fontFamily, fontSize, foreColor, backColor, lineHeight, letterSpacing, paragraphSpacing |
261
+ | `registerMediaCommands` | embedMedia, removeEmbed |
262
+ | `registerFindReplaceCommands` | find, findNext, findPrev, replace, replaceAll |
263
+ | `registerSourceModeCommands` | sourceMode |
264
+ | `registerFullscreenCommands` | fullscreen |
265
+ | `registerDistractionFreeCommands` | distractionFree |
266
+ | `registerSplitViewCommands` | toggleSplitView |
267
+ | `registerColorPresetCommands` | saveColorPreset, loadColorPresets, deleteColorPreset |
268
+ | `registerMarkdownToggleCommands` | toggleMarkdown |
269
+ | `registerAttachmentCommands` | insertAttachment, removeAttachment |
270
+ | `registerImportDocumentCommands` | importDocument |
271
+
272
+ ### Formatting
273
+
274
+ ```js
275
+ registerFormattingCommands(engine);
276
+
277
+ engine.executeCommand('bold'); // Toggle bold (Mod+B)
278
+ engine.executeCommand('italic'); // Toggle italic (Mod+I)
279
+ engine.executeCommand('underline'); // Toggle underline (Mod+U)
280
+ engine.executeCommand('strikethrough'); // Toggle strikethrough (Mod+Shift+X)
281
+ engine.executeCommand('subscript'); // Toggle subscript (Mod+,)
282
+ engine.executeCommand('superscript'); // Toggle superscript (Mod+.)
283
+ engine.executeCommand('removeFormat'); // Strip all inline formatting
284
+ ```
285
+
286
+ ### Headings
287
+
288
+ ```js
289
+ registerHeadingCommands(engine);
290
+
291
+ engine.executeCommand('heading', 1); // Apply H1
292
+ engine.executeCommand('heading', 3); // Apply H3
293
+ engine.executeCommand('heading', 'p'); // Reset to paragraph
294
+ engine.executeCommand('h2'); // Shorthand for heading level 2
295
+ engine.executeCommand('paragraph'); // Shorthand for normal text
296
+ ```
297
+
298
+ If `baseHeadingLevel` is set in options, heading levels are offset. For example, with `baseHeadingLevel: 2`, `heading(1)` renders as `<h2>`.
299
+
300
+ ### Lists
301
+
302
+ ```js
303
+ registerListCommands(engine);
304
+
305
+ engine.executeCommand('orderedList'); // Toggle numbered list (Mod+Shift+7)
306
+ engine.executeCommand('unorderedList'); // Toggle bullet list (Mod+Shift+8)
307
+ engine.executeCommand('taskList'); // Toggle task list with checkboxes
308
+ engine.executeCommand('indent'); // Increase indentation
309
+ engine.executeCommand('outdent'); // Decrease indentation
310
+ ```
311
+
312
+ ### Alignment
313
+
314
+ ```js
315
+ registerAlignmentCommands(engine);
316
+
317
+ engine.executeCommand('alignLeft');
318
+ engine.executeCommand('alignCenter');
319
+ engine.executeCommand('alignRight');
320
+ engine.executeCommand('alignJustify');
321
+ ```
322
+
323
+ ### Links
324
+
325
+ ```js
326
+ registerLinkCommands(engine);
327
+
328
+ // Insert a new link (Mod+K)
329
+ engine.executeCommand('insertLink', {
330
+ href: 'https://example.com',
331
+ text: 'Example', // optional — uses selection if omitted
332
+ target: '_blank', // optional
333
+ });
334
+
335
+ // Edit an existing link
336
+ engine.executeCommand('editLink', {
337
+ href: 'https://new-url.com',
338
+ text: 'New text', // optional
339
+ target: '_self', // optional
340
+ });
341
+
342
+ // Remove link, keep text
343
+ engine.executeCommand('removeLink');
344
+ ```
345
+
346
+ ### Images
347
+
348
+ ```js
349
+ registerImageCommands(engine);
350
+
351
+ // Insert image
352
+ engine.executeCommand('insertImage', {
353
+ src: 'https://example.com/photo.jpg',
354
+ alt: 'A photo', // optional
355
+ width: 400, // optional
356
+ height: 300, // optional
357
+ });
358
+
359
+ // Resize an existing image
360
+ engine.executeCommand('resizeImage', {
361
+ element: imgElement,
362
+ width: 200,
363
+ height: 150,
364
+ });
365
+
366
+ // Align image
367
+ engine.executeCommand('alignImage', {
368
+ element: imgElement,
369
+ alignment: 'center', // 'left', 'right', 'center'
370
+ });
371
+
372
+ // Remove image
373
+ engine.executeCommand('removeImage', { element: imgElement });
374
+ ```
375
+
376
+ ### Tables
377
+
378
+ ```js
379
+ registerTableCommands(engine);
380
+
381
+ // Insert a 4x3 table (first row is <thead> with <th> cells)
382
+ engine.executeCommand('insertTable', { rows: 4, cols: 3 });
383
+
384
+ // Row operations
385
+ engine.executeCommand('addRowBefore');
386
+ engine.executeCommand('addRowAfter');
387
+ engine.executeCommand('deleteRow');
388
+
389
+ // Column operations
390
+ engine.executeCommand('addColBefore');
391
+ engine.executeCommand('addColAfter');
392
+ engine.executeCommand('deleteCol');
393
+
394
+ // Toggle header row (convert first row to/from <thead>)
395
+ engine.executeCommand('toggleHeaderRow');
396
+
397
+ // Sort by column (physically reorders rows, sets data-sort-dir on <th>)
398
+ engine.executeCommand('sortTable', { columnIndex: 0, direction: 'asc' });
399
+ engine.executeCommand('sortTable', { columnIndex: 0, direction: 'desc', dataType: 'numeric' });
400
+
401
+ // Multi-column sort
402
+ engine.executeCommand('sortTable', {
403
+ keys: [
404
+ { columnIndex: 0, direction: 'asc' },
405
+ { columnIndex: 1, direction: 'desc', dataType: 'numeric' },
406
+ ],
407
+ });
408
+
409
+ // Filter rows (non-destructive, hides non-matching rows)
410
+ engine.executeCommand('filterTable', { columnIndex: 0, filterValue: 'search term' });
411
+ engine.executeCommand('clearTableFilters');
412
+
413
+ // Cell formatting (stores raw value in data-raw-value, displays formatted)
414
+ engine.executeCommand('formatCell', { format: 'number' });
415
+ engine.executeCommand('formatCell', { format: 'currency', options: { currency: 'EUR' } });
416
+ engine.executeCommand('formatCell', { format: 'percentage' });
417
+ engine.executeCommand('formatCell', { format: 'date', options: { dateStyle: 'long' } });
418
+
419
+ // Formula evaluation (cells with data-formula attribute)
420
+ engine.executeCommand('evaluateFormulas');
421
+
422
+ // Merge and split
423
+ engine.executeCommand('mergeCells', { cells: [cell1, cell2] });
424
+ engine.executeCommand('splitCell');
425
+ ```
426
+
427
+ #### Table command reference
428
+
429
+ | Command | Arguments | Description |
430
+ | --- | --- | --- |
431
+ | `insertTable` | `{ rows, cols }` | Insert a table with `<thead>` header row. Default 3x3. |
432
+ | `addRowBefore` | — | Insert a row above the current cell |
433
+ | `addRowAfter` | — | Insert a row below the current cell |
434
+ | `addColBefore` | — | Insert a column to the left |
435
+ | `addColAfter` | — | Insert a column to the right |
436
+ | `deleteRow` | — | Delete the current row (removes table if last row) |
437
+ | `deleteCol` | — | Delete the current column (removes table if last column) |
438
+ | `deleteTable` | — | Delete the entire table |
439
+ | `mergeCells` | `{ cells: [el, el, ...] }` | Merge an array of cell elements |
440
+ | `splitCell` | — | Split a merged cell back into individual cells |
441
+ | `toggleHeaderRow` | — | Convert first row to/from `<thead>` with `<th>` cells |
442
+ | `sortTable` | `{ columnIndex, direction, dataType }` or `{ keys: [...] }` | Sort rows. Direction: `'asc'` or `'desc'`. DataType: `'alphabetical'`, `'numeric'`, or `'date'` (auto-detected if omitted). Use `keys` array for multi-column sort. |
443
+ | `filterTable` | `{ columnIndex, filterValue }` | Hide rows where the cell at `columnIndex` doesn't contain `filterValue` (case-insensitive substring match). Pass empty string to clear a single column filter. |
444
+ | `clearTableFilters` | — | Remove all column filters and show all rows |
445
+ | `formatCell` | `{ format, options }` | Format the focused cell. Format: `'number'`, `'currency'`, `'percentage'`, `'date'`. Options: `{ decimals, currency, dateStyle }`. |
446
+ | `evaluateFormulas` | — | Re-evaluate all formula cells in the focused table |
447
+
448
+ #### Sort data types
449
+
450
+ The sort command auto-detects the data type of a column by sampling its values:
451
+ - **numeric** — if >70% of values parse as numbers
452
+ - **date** — if >70% of values parse as valid dates
453
+ - **alphabetical** — default, uses locale-aware `localeCompare`
454
+
455
+ You can override auto-detection by passing `dataType` explicitly, or provide a global custom comparator via `engine.options.tableSortComparator`:
456
+
457
+ ```js
458
+ engine.options.tableSortComparator = (a, b, dataType, columnIndex) => {
459
+ // Custom comparison — return negative, zero, or positive
460
+ return a.localeCompare(b, 'de'); // German locale sort
461
+ };
462
+ ```
463
+
464
+ #### Formulas
465
+
466
+ Cells starting with `=` are treated as formulas when the `TablePlugin` is active. On plugin initialization, cells with `=` prefix text are automatically detected and converted to formula cells (the `data-formula` attribute is added automatically — you do not need to set it manually in your HTML). The leading `=` is stripped before evaluation, and numeric results are rounded to 10 decimal places to avoid floating point display artifacts (e.g., `249.95` instead of `249.95000000000002`). On focus, the formula text is shown for editing; on blur, it is re-evaluated.
467
+
468
+ **Supported functions:**
469
+
470
+ | Function | Description | Example |
471
+ | --- | --- | --- |
472
+ | `SUM` | Sum all values in a range | `=SUM(A1:A10)` |
473
+ | `AVERAGE` | Arithmetic mean of values | `=AVERAGE(B2:B8)` |
474
+ | `COUNT` | Count non-empty cells | `=COUNT(A1:A20)` |
475
+ | `MIN` | Smallest value in a range | `=MIN(C1:C5)` |
476
+ | `MAX` | Largest value in a range | `=MAX(C1:C5)` |
477
+ | `IF` | Conditional value | `=IF(A1>10, "high", "low")` |
478
+ | `CONCAT` | Join values into a string | `=CONCAT(A1, " ", B1)` |
479
+
480
+ **Cell references:** A1 notation (e.g., `A1`, `B3`, `AA1`), ranges (e.g., `A1:A5`, `B2:D4`)
481
+
482
+ **Operators:** `+`, `-`, `*`, `/`, `>`, `<`, `>=`, `<=`, `==`
483
+
484
+ **Formula examples:**
485
+
486
+ ```
487
+ =SUM(A2:A10) Sum of column A, rows 2-10
488
+ =AVERAGE(B2:B8) Average of column B, rows 2-8
489
+ =A1+B1*2 Arithmetic with cell references
490
+ =IF(A1>100, "over", "under") Conditional logic
491
+ =MAX(A1:A5)-MIN(A1:A5) Range (max minus min)
492
+ =CONCAT(A1, " - ", B1) String concatenation
493
+ =COUNT(A1:D1) Count non-empty cells in a row
494
+ ```
495
+
496
+ **Circular reference detection:** If cell A1 references B1, and B1 references A1, both cells display `#CIRC!`.
497
+
498
+ **Programmatic evaluation:** Call `evaluateTableFormulas(tableElement)` to re-evaluate all formula cells in a specific table element without needing selection context.
499
+
500
+ ```js
501
+ import { evaluateTableFormulas } from '@remyxjs/core';
502
+
503
+ const table = document.querySelector('table.rmx-table');
504
+ evaluateTableFormulas(table);
505
+ ```
506
+
507
+ #### Cell formatting
508
+
509
+ The `formatCell` command uses the browser's built-in `Intl` APIs for locale-aware formatting:
510
+
511
+ ```js
512
+ // Number: "1,234.50"
513
+ engine.executeCommand('formatCell', { format: 'number', options: { decimals: 2 } });
514
+
515
+ // Currency: "$1,234.50" (or locale equivalent)
516
+ engine.executeCommand('formatCell', { format: 'currency', options: { currency: 'USD' } });
517
+
518
+ // Euro: "1.234,50 €"
519
+ engine.executeCommand('formatCell', { format: 'currency', options: { currency: 'EUR' } });
520
+
521
+ // Percentage: "75.0%" (raw value 0.75 × 100)
522
+ engine.executeCommand('formatCell', { format: 'percentage', options: { decimals: 1 } });
523
+
524
+ // Date: locale-formatted date
525
+ engine.executeCommand('formatCell', { format: 'date', options: { dateStyle: 'long' } });
526
+ ```
527
+
528
+ The raw value is preserved in the `data-raw-value` attribute so it can be used for sorting and formula calculations even after formatting.
529
+
530
+ #### Clipboard interop
531
+
532
+ When copying from a Remyx table, the clipboard contains both:
533
+ - `text/html` — clean `<table>` markup
534
+ - `text/plain` — TSV (tab-separated values) for pasting into spreadsheets
535
+
536
+ When pasting into a table cell:
537
+ - **TSV data** (from Excel, Sheets, or tab-separated text) is detected and inserted into the grid starting at the caret cell
538
+ - **HTML tables** (from Excel or Sheets) are converted to TSV and inserted the same way
539
+ - Rows and columns are automatically added if the pasted data exceeds the current table dimensions
540
+
541
+ Google Sheets `<google-sheets-html-origin>` tags and Excel `mso-*` styles are automatically stripped during paste.
542
+
543
+ ### Blocks
544
+
545
+ ```js
546
+ registerBlockCommands(engine);
547
+
548
+ engine.executeCommand('blockquote'); // Toggle blockquote (Mod+Shift+9)
549
+ engine.executeCommand('codeBlock'); // Toggle code block (Mod+Shift+C)
550
+ engine.executeCommand('codeBlock', { language: 'javascript' }); // Code block with language
551
+ engine.executeCommand('horizontalRule'); // Insert <hr>
552
+ ```
553
+
554
+ ### Fonts
555
+
556
+ ```js
557
+ registerFontCommands(engine);
558
+
559
+ engine.executeCommand('fontFamily', 'Georgia');
560
+ engine.executeCommand('fontSize', '18px'); // Accepts px, pt, em, rem, %
561
+ engine.executeCommand('foreColor', '#ff0000');
562
+ engine.executeCommand('backColor', '#ffff00');
563
+ ```
564
+
565
+ ### Media Embeds
566
+
567
+ ```js
568
+ registerMediaCommands(engine);
569
+
570
+ // Embed a YouTube, Vimeo, or Dailymotion video
571
+ engine.executeCommand('embedMedia', {
572
+ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
573
+ });
574
+
575
+ // Remove an embed
576
+ engine.executeCommand('removeEmbed', { element: embedElement });
577
+ ```
578
+
579
+ ### Find & Replace
580
+
581
+ ```js
582
+ registerFindReplaceCommands(engine);
583
+
584
+ // Search (Mod+F)
585
+ engine.executeCommand('find', {
586
+ text: 'hello',
587
+ caseSensitive: false, // optional, default false
588
+ });
589
+
590
+ engine.executeCommand('findNext');
591
+ engine.executeCommand('findPrev');
592
+
593
+ // Replace current match
594
+ engine.executeCommand('replace', { replaceText: 'world' });
595
+
596
+ // Replace all matches
597
+ engine.executeCommand('replaceAll', {
598
+ searchText: 'hello',
599
+ replaceText: 'world',
600
+ });
601
+
602
+ // Listen for results
603
+ engine.on('find:results', ({ total, current }) => {
604
+ console.log(`Match ${current + 1} of ${total}`);
605
+ });
606
+ ```
607
+
608
+ ### Source Mode
609
+
610
+ ```js
611
+ registerSourceModeCommands(engine);
612
+
613
+ // Toggle HTML source view (Mod+Shift+U)
614
+ engine.executeCommand('sourceMode');
615
+
616
+ engine.on('mode:change', ({ sourceMode }) => {
617
+ console.log('Source mode:', sourceMode);
618
+ });
619
+ ```
620
+
621
+ ### Fullscreen
622
+
623
+ ```js
624
+ registerFullscreenCommands(engine);
625
+
626
+ // Toggle fullscreen (Mod+Shift+F)
627
+ engine.executeCommand('fullscreen');
628
+
629
+ engine.on('fullscreen:toggle', ({ fullscreen }) => {
630
+ console.log('Fullscreen:', fullscreen);
631
+ });
632
+ ```
633
+
634
+ ### Distraction-Free Mode
635
+ ```js
636
+ registerDistractionFreeCommands(engine);
637
+
638
+ // Toggle distraction-free mode (Mod+Shift+D)
639
+ engine.executeCommand('distractionFree');
640
+ ```
641
+ Hides toolbar, status bar, and menu bar. Chrome reappears on mouse movement and auto-hides after 3 seconds of inactivity. Adds `.rmx-distraction-free` class to the editor root.
642
+
643
+ ### Split View
644
+ ```js
645
+ registerSplitViewCommands(engine);
646
+
647
+ // Toggle split view (Mod+Shift+V)
648
+ engine.executeCommand('toggleSplitView');
649
+ ```
650
+ Opens a side-by-side preview pane showing rendered HTML or markdown output. Adds `.rmx-split-view` class to the editor root.
651
+
652
+ ### Color Presets
653
+ ```js
654
+ registerColorPresetCommands(engine);
655
+
656
+ // Save a named color preset (persisted in localStorage)
657
+ engine.executeCommand('saveColorPreset', { name: 'Brand', colors: ['#e11d48', '#3b82f6', '#22c55e'] });
658
+
659
+ // Load all saved presets
660
+ const presets = engine.executeCommand('loadColorPresets');
661
+
662
+ // Delete a preset
663
+ engine.executeCommand('deleteColorPreset', 'Brand');
664
+ ```
665
+
666
+ ### Typography Controls
667
+ ```js
668
+ // Line height (applied as inline style to selected text)
669
+ engine.executeCommand('lineHeight', '1.8');
670
+
671
+ // Letter spacing
672
+ engine.executeCommand('letterSpacing', '0.05em');
673
+
674
+ // Paragraph spacing (margin-bottom on block elements)
675
+ engine.executeCommand('paragraphSpacing', '1.5em');
676
+ ```
677
+ These commands are registered by `registerFontCommands(engine)`. A `typography` toolbar dropdown provides UI access to all three.
678
+
679
+ ### Sticky Toolbar
680
+ The toolbar uses `position: sticky; top: 0` by default, remaining visible when scrolling long documents. No configuration needed.
681
+
682
+ ### Markdown Toggle
683
+
684
+ ```js
685
+ registerMarkdownToggleCommands(engine);
686
+
687
+ // Toggle between rich-text and markdown editing
688
+ engine.executeCommand('toggleMarkdown');
689
+
690
+ engine.on('mode:change:markdown', ({ markdownMode }) => {
691
+ console.log('Markdown mode:', markdownMode);
692
+ });
693
+ ```
694
+
695
+ ### Attachments
696
+
697
+ ```js
698
+ registerAttachmentCommands(engine);
699
+
700
+ engine.executeCommand('insertAttachment', {
701
+ url: 'https://example.com/report.pdf',
702
+ filename: 'report.pdf',
703
+ filesize: '2.4 MB', // optional, displayed in UI
704
+ });
705
+
706
+ engine.executeCommand('removeAttachment', { element: attachmentElement });
707
+ ```
708
+
709
+ ### Document Import
710
+
711
+ ```js
712
+ registerImportDocumentCommands(engine);
713
+
714
+ // Opens a native file picker for supported formats
715
+ engine.executeCommand('importDocument');
716
+ ```
717
+
718
+ ### Command Palette
719
+
720
+ The command palette provides a searchable overlay listing all available editor commands. It is a React-layer feature (see `@remyxjs/react`), but the command catalog and filter logic live in `@remyxjs/core`:
721
+
722
+ ```js
723
+ import { SLASH_COMMAND_ITEMS, filterSlashItems } from '@remyxjs/core';
724
+
725
+ // Default catalog of ~30 command items across 6 categories:
726
+ // Text, Lists, Media, Layout, Insert, Advanced
727
+ // Insert category includes plugin commands: Callout, Math Equation,
728
+ // Table of Contents, Bookmark, Merge Tag, Comment
729
+ console.log(SLASH_COMMAND_ITEMS);
730
+
731
+ // Filter items by query (fuzzy substring match on label, description, keywords)
732
+ const matches = filterSlashItems(SLASH_COMMAND_ITEMS, 'head');
733
+ // → [{ id: 'heading1', label: 'Heading 1', ... }, { id: 'heading2', ... }, ...]
734
+ ```
735
+
736
+ Each item has the shape `{ id, label, description, icon, keywords, category, action }`. The `action` function receives `(engine, openModal?)` and executes the command.
737
+
738
+ #### Recently-used commands
739
+
740
+ The last 5 executed commands are tracked in `localStorage` and pinned to the top of the palette under a "Recent" category when no search query is active:
741
+
742
+ ```js
743
+ import { getRecentCommands, recordRecentCommand, clearRecentCommands } from '@remyxjs/core';
744
+
745
+ // Commands are recorded automatically when executed via the palette.
746
+ // You can also record manually:
747
+ recordRecentCommand('heading1');
748
+
749
+ // Read the recent list (most recent first, max 5)
750
+ getRecentCommands(); // → ['heading1']
751
+
752
+ // Clear the history
753
+ clearRecentCommands();
754
+
755
+ // filterSlashItems pins recent items by default (disable with { pinRecent: false })
756
+ filterSlashItems(SLASH_COMMAND_ITEMS, ''); // recent items at top
757
+ filterSlashItems(SLASH_COMMAND_ITEMS, '', { pinRecent: false }); // no pinning
758
+ ```
759
+
760
+ #### Custom command items
761
+
762
+ Register custom command items that appear alongside built-in commands in the palette:
763
+
764
+ ```js
765
+ import { registerCommandItems, unregisterCommandItem, getCustomCommandItems } from '@remyxjs/core';
766
+
767
+ // Register a single item
768
+ registerCommandItems({
769
+ id: 'insertSignature',
770
+ label: 'Insert Signature',
771
+ description: 'Add your email signature',
772
+ icon: '✍️',
773
+ keywords: ['signature', 'sign', 'email'],
774
+ category: 'Custom',
775
+ action: (engine) => engine.executeCommand('insertHTML', '<p>— John Doe</p>'),
776
+ });
777
+
778
+ // Register multiple items at once
779
+ registerCommandItems([
780
+ { id: 'draft', label: 'Save Draft', description: 'Save as draft', icon: '💾', keywords: ['save', 'draft'], category: 'Custom', action: (engine) => saveDraft(engine.getHTML()) },
781
+ { id: 'publish', label: 'Publish', description: 'Publish document', icon: '🚀', keywords: ['publish', 'post'], category: 'Custom', action: (engine) => publish(engine.getHTML()) },
782
+ ]);
783
+
784
+ // Re-registering the same id replaces the previous item
785
+ registerCommandItems({ id: 'insertSignature', label: 'Insert Sig (updated)', /* ... */ });
786
+
787
+ // Remove a custom item
788
+ unregisterCommandItem('insertSignature');
789
+
790
+ // Get all registered custom items
791
+ getCustomCommandItems(); // → [{ id: 'draft', ... }, { id: 'publish', ... }]
792
+ ```
793
+
794
+ ## Autosave
795
+
796
+ Framework-agnostic autosave engine with pluggable storage providers. Debounces saves after content changes, runs periodic interval saves, and detects recoverable content on startup.
797
+
798
+ ### Storage Providers
799
+
800
+ Five built-in providers cover browser storage, filesystem, cloud, and custom backends:
801
+
802
+ ```js
803
+ import {
804
+ LocalStorageProvider,
805
+ SessionStorageProvider,
806
+ FileSystemProvider,
807
+ CloudProvider,
808
+ CustomProvider,
809
+ createStorageProvider,
810
+ } from '@remyxjs/core';
811
+ ```
812
+
813
+ | Provider | Use Case | Config Shorthand |
814
+ | --- | --- | --- |
815
+ | `LocalStorageProvider` | Browser apps (default) | `'localStorage'` or omit |
816
+ | `SessionStorageProvider` | Tab-scoped saves | `'sessionStorage'` |
817
+ | `FileSystemProvider` | Node / Electron / Tauri | `{ writeFn, readFn, deleteFn }` |
818
+ | `CloudProvider` | AWS S3, GCP, any HTTP API | `{ endpoint, headers, ... }` |
819
+ | `CustomProvider` | Full consumer control | `{ save, load, clear }` |
820
+
821
+ Each provider implements `save(key, content)`, `load(key)`, and `clear(key)`. Content is wrapped in a JSON envelope with `{ content, timestamp, version }`.
822
+
823
+ **Factory function** — `createStorageProvider(config)` resolves shorthand strings or objects into provider instances:
824
+
825
+ ```js
826
+ const local = createStorageProvider(); // LocalStorageProvider
827
+ const session = createStorageProvider('sessionStorage'); // SessionStorageProvider
828
+ const cloud = createStorageProvider({ // CloudProvider
829
+ endpoint: 'https://api.example.com/autosave',
830
+ headers: { Authorization: 'Bearer token123' },
831
+ });
832
+ const fs = createStorageProvider({ // FileSystemProvider
833
+ writeFn: async (key, data) => writeFile(`/saves/${key}.json`, data),
834
+ readFn: async (key) => readFile(`/saves/${key}.json`),
835
+ deleteFn: async (key) => unlink(`/saves/${key}.json`),
836
+ });
837
+ ```
838
+
839
+ **CloudProvider options** for AWS S3 / GCP / custom APIs:
840
+
841
+ ```js
842
+ const s3Provider = new CloudProvider({
843
+ endpoint: 'https://my-bucket.s3.amazonaws.com',
844
+ buildUrl: (key) => getPresignedUploadUrl(key), // S3 presigned URL
845
+ buildLoadUrl: (key) => getPresignedDownloadUrl(key),
846
+ method: 'PUT',
847
+ headers: { 'Content-Type': 'application/json' },
848
+ fetchFn: fetch, // optional custom fetch
849
+ });
850
+ ```
851
+
852
+ ### AutosaveManager API
853
+
854
+ ```js
855
+ import { AutosaveManager } from '@remyxjs/core';
856
+
857
+ const manager = new AutosaveManager(engine, {
858
+ provider: 'localStorage', // or any provider config
859
+ key: 'doc-123', // storage key (default: 'rmx-default')
860
+ interval: 30000, // periodic save interval in ms (default: 30s)
861
+ debounce: 2000, // debounce delay after content change (default: 2s)
862
+ enabled: true, // toggle on/off (default: true)
863
+ });
864
+
865
+ manager.init(); // start listening to content:change
866
+
867
+ await manager.save(); // force an immediate save
868
+ await manager.checkRecovery(engine.getHTML()); // check for recoverable content
869
+ await manager.clearRecovery(); // clear stored recovery data
870
+
871
+ manager.destroy(); // cleanup timers, listeners, final save
872
+ ```
873
+
874
+ ### Autosave Events
875
+
876
+ ```js
877
+ engine.eventBus.on('autosave:saving', () => {
878
+ console.log('Saving...');
879
+ });
880
+
881
+ engine.eventBus.on('autosave:saved', ({ timestamp }) => {
882
+ console.log(`Saved at ${new Date(timestamp).toLocaleTimeString()}`);
883
+ });
884
+
885
+ engine.eventBus.on('autosave:error', ({ error }) => {
886
+ console.error('Autosave failed:', error.message);
887
+ });
888
+
889
+ engine.eventBus.on('autosave:recovered', ({ recoveredContent, timestamp }) => {
890
+ if (confirm('Unsaved changes found. Restore?')) {
891
+ engine.setHTML(recoveredContent);
892
+ }
893
+ });
894
+ ```
895
+
896
+ ## Plugin System
897
+
898
+ ### Creating Plugins
899
+
900
+ ```js
901
+ import { createPlugin } from '@remyxjs/core';
902
+
903
+ const HighlightPlugin = createPlugin({
904
+ name: 'highlight',
905
+
906
+ init(api) {
907
+ // api is a restricted PluginAPI (see below)
908
+ api.on('selection:change', (formats) => {
909
+ // React to selection changes
910
+ });
911
+ },
912
+
913
+ destroy(api) {
914
+ // Cleanup (event listeners registered via api.on are auto-cleaned)
915
+ },
916
+
917
+ // Optional: add commands
918
+ commands: [
919
+ {
920
+ name: 'highlight',
921
+ execute(engine) {
922
+ document.execCommand('hiliteColor', false, 'yellow');
923
+ },
924
+ isActive(engine) {
925
+ return engine.selection.getActiveFormats().backColor === 'yellow';
926
+ },
927
+ shortcut: 'mod+shift+h',
928
+ },
929
+ ],
930
+
931
+ // Optional: add toolbar buttons
932
+ toolbarItems: [
933
+ {
934
+ name: 'highlight',
935
+ command: 'highlight',
936
+ icon: '🖍',
937
+ tooltip: 'Highlight',
938
+ group: 'formatting',
939
+ },
940
+ ],
941
+
942
+ // Optional: add status bar items
943
+ statusBarItems: [
944
+ {
945
+ name: 'highlight-status',
946
+ render: (engine) => engine.selection.getActiveFormats().backColor || 'none',
947
+ },
948
+ ],
949
+
950
+ // Optional: add context menu items
951
+ contextMenuItems: [
952
+ {
953
+ name: 'highlight-menu',
954
+ label: 'Highlight selection',
955
+ command: 'highlight',
956
+ },
957
+ ],
958
+ });
959
+
960
+ // Register the plugin
961
+ engine.plugins.register(HighlightPlugin);
962
+ ```
963
+
964
+ **Full engine access:** By default, plugins receive a restricted API. If your plugin needs direct engine access, set `requiresFullAccess: true`:
965
+
966
+ ```js
967
+ const AdvancedPlugin = createPlugin({
968
+ name: 'advanced',
969
+ requiresFullAccess: true,
970
+ init(engine) {
971
+ // Full EditorEngine instance — use with care
972
+ engine.element.addEventListener('dblclick', handleDblClick);
973
+ },
974
+ destroy(engine) {
975
+ engine.element.removeEventListener('dblclick', handleDblClick);
976
+ },
977
+ });
978
+ ```
979
+
980
+ ### Lifecycle Hooks
981
+
982
+ Beyond `init`/`destroy`, plugins can declare `onContentChange` and `onSelectionChange` callbacks that are automatically wired to engine events:
983
+
984
+ ```js
985
+ const AnalyticsPlugin = createPlugin({
986
+ name: 'analytics',
987
+ onContentChange(api) {
988
+ // Called on every content:change event
989
+ trackEvent('content_edit', { length: api.getText().length });
990
+ },
991
+ onSelectionChange(api) {
992
+ // Called on every selectionchange event
993
+ const formats = api.getActiveFormats();
994
+ updateToolbarState(formats);
995
+ },
996
+ });
997
+ ```
998
+
999
+ Each lifecycle callback is sandboxed — if it throws, the error is caught, logged, and emitted as a `plugin:error` event without affecting other plugins.
1000
+
1001
+ ### Plugin Dependencies
1002
+
1003
+ Declare dependencies to control initialization order:
1004
+
1005
+ ```js
1006
+ const BasePlugin = createPlugin({ name: 'base', init() { /* ... */ } });
1007
+
1008
+ const ExtensionPlugin = createPlugin({
1009
+ name: 'extension',
1010
+ dependencies: ['base'], // initialized after 'base'
1011
+ init(api) { /* can safely use base's commands */ },
1012
+ });
1013
+ ```
1014
+
1015
+ Dependencies are resolved using topological sort. Circular dependencies are detected and reported via `plugin:circularDependency` events. Missing dependencies are silently skipped.
1016
+
1017
+ ### Scoped Plugin Settings
1018
+
1019
+ Plugins can define a settings schema with type validation:
1020
+
1021
+ ```js
1022
+ const ThemePlugin = createPlugin({
1023
+ name: 'custom-theme',
1024
+ settingsSchema: [
1025
+ { key: 'fontSize', type: 'number', label: 'Font Size', defaultValue: 16, validate: (v) => v >= 8 && v <= 72 },
1026
+ { key: 'fontFamily', type: 'string', label: 'Font Family', defaultValue: 'sans-serif' },
1027
+ { key: 'mode', type: 'select', label: 'Mode', defaultValue: 'light', options: [
1028
+ { label: 'Light', value: 'light' },
1029
+ { label: 'Dark', value: 'dark' },
1030
+ ]},
1031
+ ],
1032
+ defaultSettings: { fontSize: 16, fontFamily: 'sans-serif', mode: 'light' },
1033
+ init(api) {
1034
+ const size = api.getSetting('fontSize');
1035
+ api.element.style.fontSize = `${size}px`;
1036
+ },
1037
+ });
1038
+
1039
+ // Settings are accessible from outside the plugin:
1040
+ engine.plugins.getPluginSetting('custom-theme', 'fontSize'); // 16
1041
+ engine.plugins.setPluginSetting('custom-theme', 'fontSize', 18); // validates + emits event
1042
+ engine.plugins.getPluginSettings('custom-theme'); // { fontSize: 18, fontFamily: 'sans-serif', mode: 'light' }
1043
+ ```
1044
+
1045
+ ### Plugin Registry
1046
+
1047
+ A global registry for plugin discovery and marketplace concepts:
1048
+
1049
+ ```js
1050
+ import { registerPluginInRegistry, searchPluginRegistry, listRegisteredPlugins } from '@remyxjs/core';
1051
+
1052
+ // Register a plugin for discovery
1053
+ registerPluginInRegistry({
1054
+ name: 'math-equations',
1055
+ version: '1.2.0',
1056
+ description: 'LaTeX/KaTeX math rendering',
1057
+ author: 'Community',
1058
+ tags: ['math', 'latex', 'katex', 'equations'],
1059
+ factory: () => MathPlugin(),
1060
+ });
1061
+
1062
+ // Search the registry
1063
+ searchPluginRegistry('math'); // → [{ name: 'math-equations', ... }]
1064
+ searchPluginRegistry('latex'); // → [{ name: 'math-equations', ... }] (matches tags)
1065
+ listRegisteredPlugins(); // → all registered entries
1066
+
1067
+ // Install from registry
1068
+ const entry = searchPluginRegistry('math')[0];
1069
+ engine.plugins.register(entry.factory());
1070
+ ```
1071
+
1072
+ ### Plugin Metadata
1073
+
1074
+ Plugins can include metadata for documentation and registry integration:
1075
+
1076
+ ```js
1077
+ const MyPlugin = createPlugin({
1078
+ name: 'my-plugin',
1079
+ version: '2.1.0',
1080
+ description: 'Adds custom formatting options',
1081
+ author: 'Your Name',
1082
+ // ... rest of plugin definition
1083
+ });
1084
+ ```
1085
+
1086
+ ### Plugin API (Restricted)
1087
+
1088
+ Plugins without `requiresFullAccess` receive a sandboxed API:
1089
+
1090
+ | Property/Method | Description |
1091
+ | --- | --- |
1092
+ | `element` | Editor DOM element (read-only) |
1093
+ | `options` | Engine options (read-only copy) |
1094
+ | `executeCommand(name, ...args)` | Execute a command |
1095
+ | `on(event, handler)` | Subscribe to events |
1096
+ | `off(event, handler)` | Unsubscribe |
1097
+ | `getSelection()` | Browser Selection object |
1098
+ | `getRange()` | Current Range in editor |
1099
+ | `getActiveFormats()` | Current formatting state |
1100
+ | `getHTML()` | Get content as HTML |
1101
+ | `getText()` | Get content as plain text |
1102
+ | `isEmpty()` | Check if editor is empty |
1103
+ | `getSetting(key)` | Get a plugin-scoped setting value |
1104
+ | `setSetting(key, value)` | Set a plugin-scoped setting value (with validation) |
1105
+
1106
+ ### Built-in Plugins
1107
+
1108
+ **WordCountPlugin** — Emits `wordcount:update` with `{ wordCount, charCount }` on every content change.
1109
+
1110
+ ```js
1111
+ import { WordCountPlugin } from '@remyxjs/core';
1112
+ engine.plugins.register(WordCountPlugin);
1113
+ engine.on('wordcount:update', ({ wordCount, charCount }) => {
1114
+ document.querySelector('#count').textContent = `${wordCount} words`;
1115
+ });
1116
+ ```
1117
+
1118
+ **AutolinkPlugin** — Automatically converts typed URLs into clickable links when the user presses Space or Enter.
1119
+
1120
+ ```js
1121
+ import { AutolinkPlugin } from '@remyxjs/core';
1122
+ engine.plugins.register(AutolinkPlugin);
1123
+ ```
1124
+
1125
+ **PlaceholderPlugin** — Shows placeholder text when the editor is empty.
1126
+
1127
+ ```js
1128
+ import { PlaceholderPlugin } from '@remyxjs/core';
1129
+ engine.plugins.register(PlaceholderPlugin('Start writing...'));
1130
+ ```
1131
+
1132
+ **SyntaxHighlightPlugin** — Automatic syntax highlighting for `<pre><code>` blocks. Detects language from `data-language` attribute or auto-detects from content. Highlights using `.rmx-syn-*` CSS classes that adapt to all built-in themes. Includes line numbers toggle, copy-to-clipboard button, inline code highlighting, and an extensible language registry.
1133
+
1134
+ ```js
1135
+ import {
1136
+ SyntaxHighlightPlugin, SUPPORTED_LANGUAGES, detectLanguage, tokenize,
1137
+ registerLanguage, unregisterLanguage, runRules,
1138
+ } from '@remyxjs/core';
1139
+
1140
+ // Register the plugin
1141
+ engine.plugins.register(SyntaxHighlightPlugin());
1142
+
1143
+ // Set language on the focused code block
1144
+ engine.executeCommand('setCodeLanguage', { language: 'python' });
1145
+
1146
+ // Get language of the focused code block
1147
+ const lang = engine.executeCommand('getCodeLanguage'); // 'python' or null
1148
+
1149
+ // Toggle line numbers on the focused code block
1150
+ engine.executeCommand('toggleLineNumbers');
1151
+
1152
+ // Available languages (for building UI dropdowns)
1153
+ console.log(SUPPORTED_LANGUAGES);
1154
+ // [{ id: 'javascript', label: 'JavaScript' }, { id: 'python', label: 'Python' }, ...]
1155
+
1156
+ // Auto-detect language from code content
1157
+ const detected = detectLanguage('def hello():\n print("hi")');
1158
+ // 'python'
1159
+
1160
+ // Tokenize code programmatically
1161
+ const tokens = tokenize('const x = 42', 'javascript');
1162
+ // [{ type: 'keyword', value: 'const' }, { type: 'plain', value: ' x ' }, ...]
1163
+ ```
1164
+
1165
+ **Supported languages:** JavaScript/TypeScript, Python, CSS, SQL, JSON, Bash/Shell, Rust, Go, Java, HTML/XML. Language aliases are supported (e.g., `js`, `ts`, `tsx`, `py`, `sh`, `rs`, `golang`).
1166
+
1167
+ #### Line numbers
1168
+
1169
+ Add the `data-line-numbers` attribute to any `<pre>` element (or use the `toggleLineNumbers` command) to show a line number gutter. Line numbers update automatically when the code content changes.
1170
+
1171
+ ```js
1172
+ engine.executeCommand('toggleLineNumbers'); // Toggle on the focused code block
1173
+ ```
1174
+
1175
+ #### Copy-to-clipboard
1176
+
1177
+ Every code block automatically gets a copy button (top-right corner, visible on hover). The button uses the async Clipboard API with an `execCommand('copy')` fallback for insecure contexts. A ✓ checkmark appears briefly after a successful copy.
1178
+
1179
+ #### Inline code highlighting
1180
+
1181
+ Add a `data-language` attribute to inline `<code>` elements (not inside `<pre>`) for mini syntax highlighting:
1182
+
1183
+ ```html
1184
+ <code data-language="js">const x = 42</code>
1185
+ ```
1186
+
1187
+ The inline code element will be tokenized with the same `rmx-syn-*` classes used by code blocks.
1188
+
1189
+ #### Custom language registration
1190
+
1191
+ Register custom language tokenizers at runtime. The new language immediately becomes available for highlighting and appears in `SUPPORTED_LANGUAGES`.
1192
+
1193
+ ```js
1194
+ import { registerLanguage, unregisterLanguage, runRules } from '@remyxjs/core';
1195
+
1196
+ // Define tokenizer rules (same format used by all built-in tokenizers)
1197
+ const RUBY_RULES = [
1198
+ [/#[^\n]*/g, 'rmx-syn-comment'],
1199
+ [/"(?:[^"\\]|\\.)*"/g, 'rmx-syn-string'],
1200
+ [/'(?:[^'\\]|\\.)*'/g, 'rmx-syn-string'],
1201
+ [/\b(?:def|end|class|module|if|else|elsif|unless|do|while|for|return|yield|begin|rescue|ensure)\b/g, 'rmx-syn-keyword'],
1202
+ [/\b(?:puts|print|require|include|attr_accessor|attr_reader)\b/g, 'rmx-syn-builtin'],
1203
+ [/:\w+/g, 'rmx-syn-entity'],
1204
+ [/\b\d[\d_.]*\b/g, 'rmx-syn-number'],
1205
+ ];
1206
+
1207
+ // Register with the built-in rule engine
1208
+ registerLanguage('ruby', 'Ruby', (code) => runRules(code, RUBY_RULES), ['rb']);
1209
+
1210
+ // Now works everywhere
1211
+ tokenize('puts "hello"', 'ruby'); // tokenize API
1212
+ tokenize('puts "hello"', 'rb'); // alias works too
1213
+ engine.executeCommand('setCodeLanguage', { language: 'ruby' }); // in editor
1214
+
1215
+ // Remove later if needed
1216
+ unregisterLanguage('ruby', ['rb']);
1217
+ ```
1218
+
1219
+ **TablePlugin** — Enhanced table features including column/row resize handles, click-to-sort on header cells (single + multi-column with Shift), filterable rows with per-column dropdown UI, inline cell formulas with a recursive-descent expression engine, cell formatting (number, currency, percentage, date), and sticky header rows. Uses MutationObserver to auto-detect tables and attach functionality.
1220
+
1221
+ ```js
1222
+ import { TablePlugin, evaluateTableFormulas } from '@remyxjs/core';
1223
+
1224
+ // Register the plugin
1225
+ engine.plugins.register(TablePlugin());
1226
+
1227
+ // The plugin automatically:
1228
+ // - Attaches resize handles to table column/row borders
1229
+ // - Makes <th> cells clickable for sorting (Shift+click for multi-sort)
1230
+ // - Injects filter buttons into header cells
1231
+ // - Evaluates formulas on cell blur (cells starting with '=')
1232
+ // - Re-evaluates all formulas on content change (debounced)
1233
+
1234
+ // Programmatically evaluate all formulas in a table
1235
+ evaluateTableFormulas(tableElement);
1236
+ ```
1237
+
1238
+ **CommentsPlugin** — Inline comment threads with @mention parsing, resolved/unresolved state, reply threads, comment-only mode, import/export, and DOM synchronization.
1239
+
1240
+ ```js
1241
+ import { CommentsPlugin, parseMentions } from '@remyxjs/core';
1242
+
1243
+ // Register the plugin
1244
+ engine.plugins.register(CommentsPlugin({
1245
+ onComment: (thread) => saveToServer(thread),
1246
+ onResolve: ({ thread, resolved }) => updateServer(thread),
1247
+ onDelete: (thread) => deleteFromServer(thread),
1248
+ onReply: ({ thread, reply }) => saveReply(thread.id, reply),
1249
+ mentionUsers: ['alice', 'bob', 'charlie'],
1250
+ commentOnly: false, // true = read-only editor with comment support
1251
+ }));
1252
+
1253
+ // The plugin exposes engine._comments API:
1254
+ engine._comments.addComment({ author: 'Alice', body: 'This needs clarification @bob' });
1255
+ engine._comments.resolveComment(threadId, true);
1256
+ engine._comments.replyToComment(threadId, { author: 'Bob', body: 'Fixed!' });
1257
+ engine._comments.deleteComment(threadId);
1258
+ engine._comments.navigateToComment(threadId); // scroll to + select
1259
+ engine._comments.getAllThreads(); // all threads (newest first)
1260
+ engine._comments.getUnresolvedThreads();
1261
+ engine._comments.getResolvedThreads();
1262
+ engine._comments.exportThreads(); // JSON-serializable array
1263
+ engine._comments.importThreads(data); // load from server
1264
+
1265
+ // Parse @mentions from text
1266
+ parseMentions('Hello @alice and @bob'); // → ['alice', 'bob']
1267
+ ```
1268
+
1269
+ **CalloutPlugin** — Styled callout/alert/admonition blocks with 7 built-in types, custom type registration, collapsible toggle, nested content, and GitHub-flavored alert syntax auto-conversion.
1270
+
1271
+ ```js
1272
+ import { CalloutPlugin, registerCalloutType, getCalloutTypes, parseGFMAlert } from '@remyxjs/core';
1273
+
1274
+ // Register the plugin
1275
+ engine.plugins.register(CalloutPlugin());
1276
+
1277
+ // Insert a callout at the cursor
1278
+ engine.executeCommand('insertCallout', { type: 'warning' });
1279
+ engine.executeCommand('insertCallout', { type: 'tip', collapsible: true, title: 'Pro tip' });
1280
+ engine.executeCommand('insertCallout', { type: 'info', content: '<p>Custom HTML content</p>' });
1281
+
1282
+ // Change type of the focused callout
1283
+ engine.executeCommand('changeCalloutType', 'error');
1284
+
1285
+ // Toggle collapse on the focused callout
1286
+ engine.executeCommand('toggleCalloutCollapse');
1287
+
1288
+ // Remove a callout (unwrap its content back into the editor)
1289
+ engine.executeCommand('removeCallout');
1290
+
1291
+ // Register a custom callout type
1292
+ registerCalloutType({ type: 'security', label: 'Security', icon: '🔒', color: '#dc2626' });
1293
+
1294
+ // List all types
1295
+ getCalloutTypes(); // → [{ type: 'info', ... }, { type: 'warning', ... }, ..., { type: 'security', ... }]
1296
+
1297
+ // GFM alert parsing (auto-converts blockquotes like "> [!NOTE]\nText")
1298
+ parseGFMAlert('[!WARNING]\nBe careful'); // → { type: 'warning', body: 'Be careful' }
1299
+ ```
1300
+
1301
+ **LinkPlugin** — Link previews, broken link detection, auto-linking, click analytics, bookmark anchors, and internal link suggestions.
1302
+
1303
+ ```js
1304
+ import { LinkPlugin, detectLinks, slugify } from '@remyxjs/core';
1305
+
1306
+ engine.plugins.register(LinkPlugin({
1307
+ onLinkClick: ({ href, text, timestamp }) => trackClick(href),
1308
+ onUnfurl: async (url) => {
1309
+ const res = await fetch(`/api/unfurl?url=${encodeURIComponent(url)}`);
1310
+ return res.json(); // { title, description, image }
1311
+ },
1312
+ validateLink: async (url) => {
1313
+ const res = await fetch(url, { method: 'HEAD' });
1314
+ return res.ok;
1315
+ },
1316
+ onBrokenLink: (url, el) => console.warn('Broken:', url),
1317
+ autoLink: true, // auto-convert URLs/emails/phones on Space/Enter
1318
+ showPreviews: true, // hover tooltips on links
1319
+ scanInterval: 60000, // broken link scan interval (ms)
1320
+ }));
1321
+
1322
+ // Bookmark anchors for intra-document linking
1323
+ engine.executeCommand('insertBookmark', { name: 'Introduction', id: 'intro' });
1324
+ engine.executeCommand('linkToBookmark', 'intro'); // link selected text to #intro
1325
+ engine.executeCommand('getBookmarks'); // → [{ id, name, element }]
1326
+ engine.executeCommand('scanBrokenLinks'); // manual scan
1327
+
1328
+ // Utility functions
1329
+ detectLinks('Visit https://example.com or email alice@test.com');
1330
+ // → [{ type: 'url', value: '...', index: 6 }, { type: 'email', value: '...', index: 36 }]
1331
+ slugify('Section #1: Overview!'); // → 'section-1-overview'
1332
+ ```
1333
+
1334
+ **TemplatePlugin** — Merge tags, conditional blocks, repeatable sections, live preview, and pre-built template library.
1335
+
1336
+ ```js
1337
+ import { TemplatePlugin, renderTemplate, extractTags, getTemplateLibrary } from '@remyxjs/core';
1338
+
1339
+ engine.plugins.register(TemplatePlugin());
1340
+
1341
+ // Insert a merge tag chip at the cursor
1342
+ engine.executeCommand('insertMergeTag', 'recipient_name');
1343
+
1344
+ // Load a pre-built template
1345
+ engine.executeCommand('loadTemplate', 'email');
1346
+
1347
+ // Preview with sample data (read-only mode)
1348
+ engine.executeCommand('previewTemplate', { recipient_name: 'Alice', body: 'Welcome!' });
1349
+ engine.executeCommand('exitPreview');
1350
+
1351
+ // Export as JSON
1352
+ const exported = engine.executeCommand('exportTemplate');
1353
+ // → { html: '...{{recipient_name}}...', tags: ['recipient_name', 'body'], sampleData: {...} }
1354
+
1355
+ // Render template string with data
1356
+ renderTemplate('Hello {{name}}!', { name: 'World' }); // → 'Hello World!'
1357
+ renderTemplate('{{#if show}}visible{{/if}}', { show: true }); // → 'visible'
1358
+ renderTemplate('{{#each items}}{{name}} {{/each}}', { items: [{name:'A'},{name:'B'}] }); // → 'A B '
1359
+ ```
1360
+
1361
+ **KeyboardPlugin** — Vim/Emacs modes, auto-pairing, multi-cursor, jump-to-heading.
1362
+
1363
+ ```js
1364
+ import { KeyboardPlugin, getHeadings } from '@remyxjs/core';
1365
+
1366
+ // Vim mode
1367
+ engine.plugins.register(KeyboardPlugin({ mode: 'vim' }));
1368
+
1369
+ // Emacs mode
1370
+ engine.plugins.register(KeyboardPlugin({ mode: 'emacs' }));
1371
+
1372
+ // Default with auto-pair and custom bindings
1373
+ engine.plugins.register(KeyboardPlugin({
1374
+ autoPair: true,
1375
+ keyBindings: { 'ctrl+shift+l': 'insertLink' },
1376
+ }));
1377
+
1378
+ // Multi-cursor: Cmd+D selects next occurrence
1379
+ // Jump-to-heading: Cmd+Shift+G
1380
+ // Get all headings: engine.executeCommand('getHeadings')
1381
+ ```
1382
+
1383
+ **DragDropPlugin** — Drop zones, cross-editor drag, file drops, block reorder with ghost preview. **Note:** This plugin is required for block drag handles to appear on hover. If using `BlockTemplatePlugin` for block-based editing, add `DragDropPlugin` alongside it for drag-to-reorder support.
1384
+
1385
+ ```js
1386
+ import { DragDropPlugin } from '@remyxjs/core';
1387
+
1388
+ engine.plugins.register(DragDropPlugin({
1389
+ onDrop: (event, data) => console.log('Dropped:', data.type),
1390
+ onFileDrop: (files) => uploadFiles(files),
1391
+ allowExternalDrop: true,
1392
+ showDropZone: true,
1393
+ enableReorder: true,
1394
+ }));
1395
+
1396
+ // Keyboard shortcuts for block reorder
1397
+ engine.executeCommand('moveBlockUp'); // Cmd+Shift+ArrowUp
1398
+ engine.executeCommand('moveBlockDown'); // Cmd+Shift+ArrowDown
1399
+ ```
1400
+
1401
+ **MathPlugin** — LaTeX/KaTeX math rendering with inline and block equations, symbol palette, equation numbering, and MathML export.
1402
+
1403
+ ```js
1404
+ import { MathPlugin, getSymbolPalette, parseMathExpressions, latexToMathML } from '@remyxjs/core';
1405
+
1406
+ engine.plugins.register(MathPlugin({
1407
+ renderMath: (latex, displayMode) => katex.renderToString(latex, { displayMode }), // plug in KaTeX
1408
+ }));
1409
+
1410
+ // Insert inline math
1411
+ engine.executeCommand('insertMath', { latex: 'E = mc^2', displayMode: false });
1412
+
1413
+ // Insert block equation (auto-numbered)
1414
+ engine.executeCommand('insertMath', { latex: '\\sum_{i=1}^{n} x_i', displayMode: true });
1415
+
1416
+ // Symbol palette for building UIs
1417
+ getSymbolPalette(); // → [{ category: 'Greek', symbols: [{ label: 'α', latex: '\\alpha' }, ...] }, ...]
1418
+
1419
+ // Parse math from text
1420
+ parseMathExpressions('Inline $x^2$ and block $$y^2$$');
1421
+ // → [{ type: 'block', src: 'y^2', ... }, { type: 'inline', src: 'x^2', ... }]
1422
+
1423
+ // Convert to MathML
1424
+ latexToMathML('\\frac{a}{b}'); // → '<math ...><mfrac>...</mfrac></math>'
1425
+ ```
1426
+
1427
+ **TocPlugin** — Auto-generated table of contents, document outline, heading validation, and click-to-scroll navigation.
1428
+
1429
+ ```js
1430
+ import { TocPlugin, buildOutline, flattenOutline, renderTocHTML, validateHeadingHierarchy } from '@remyxjs/core';
1431
+
1432
+ engine.plugins.register(TocPlugin({
1433
+ numbering: true,
1434
+ onOutlineChange: (outline) => updateSidebar(outline),
1435
+ }));
1436
+
1437
+ // Insert a rendered TOC into the document
1438
+ engine.executeCommand('insertToc');
1439
+
1440
+ // Get the outline programmatically
1441
+ engine.executeCommand('getOutline'); // → [{ id, text, level, number, children: [...] }]
1442
+
1443
+ // Scroll to a heading
1444
+ engine.executeCommand('scrollToHeading', 'chapter-1');
1445
+
1446
+ // Validate heading hierarchy (detect H1→H3 skips)
1447
+ engine.executeCommand('validateHeadings');
1448
+ // → [{ message: 'Heading level skipped: H1 → H3', element }]
1449
+ ```
1450
+
1451
+ **AnalyticsPlugin** — Readability scores, reading time, vocabulary level, sentence warnings, goal tracking, keyword density, and SEO hints.
1452
+
1453
+ ```js
1454
+ import { AnalyticsPlugin, analyzeContent, keywordDensity, seoAnalysis } from '@remyxjs/core';
1455
+
1456
+ engine.plugins.register(AnalyticsPlugin({
1457
+ wordsPerMinute: 200,
1458
+ targetWordCount: 1000,
1459
+ maxSentenceLength: 30,
1460
+ onAnalytics: (stats) => updateDashboard(stats),
1461
+ }));
1462
+
1463
+ // Toggle analytics panel visibility (emits 'analytics:toggle' event)
1464
+ engine.executeCommand('toggleAnalytics');
1465
+ // Available in toolbar, View menu, and command palette
1466
+
1467
+ // Get analytics
1468
+ engine.executeCommand('getAnalytics');
1469
+ // → { wordCount, charCount, sentenceCount, paragraphCount,
1470
+ // readability: { fleschKincaid, fleschReadingEase, gunningFog, colemanLiau, vocabularyLevel },
1471
+ // readingTime: { minutes, seconds, wordsPerMinute },
1472
+ // warnings: { longSentences, longParagraphs },
1473
+ // goalProgress: { target, current, percentage } }
1474
+
1475
+ // SEO analysis
1476
+ engine.executeCommand('getSeoAnalysis', 'react');
1477
+ // → { wordCount, headingCount, h1Count, keywordInfo: { count, density, positions }, hints: [...] }
1478
+
1479
+ // Keyword density
1480
+ engine.executeCommand('getKeywordDensity', 'editor');
1481
+ // → { count: 5, density: 2.3, positions: [12, 45, 78, 102, 150] }
1482
+ ```
1483
+
1484
+ **SpellcheckPlugin** — Spelling & grammar checking with inline underlines, writing-style presets, custom service integration, and persistent dictionary.
1485
+
1486
+ ```js
1487
+ import { SpellcheckPlugin, analyzeGrammar, STYLE_PRESETS } from '@remyxjs/core';
1488
+
1489
+ engine.plugins.register(SpellcheckPlugin({
1490
+ language: 'en-US', // BCP 47 language tag
1491
+ enabled: true, // enable on init
1492
+ grammarRules: true, // enable built-in grammar checking
1493
+ stylePreset: 'formal', // 'formal'|'casual'|'technical'|'academic'
1494
+ customService: { // optional external service
1495
+ check: async (text) => [...suggestions],
1496
+ },
1497
+ dictionary: ['Remyx', 'WYSIWYG'], // custom words to ignore
1498
+ persistent: true, // persist dictionary in localStorage
1499
+ onError: (errors) => {},
1500
+ onCorrection: ({ original, replacement }) => {},
1501
+ }));
1502
+
1503
+ // Toggle spellcheck on/off
1504
+ engine.executeCommand('toggleSpellcheck');
1505
+
1506
+ // Run grammar check
1507
+ engine.executeCommand('checkGrammar');
1508
+
1509
+ // Add word to dictionary
1510
+ engine.executeCommand('addToDictionary', 'Remyx');
1511
+
1512
+ // Ignore a word for this session
1513
+ engine.executeCommand('ignoreWord', 'colour');
1514
+
1515
+ // Change writing style preset
1516
+ engine.executeCommand('setWritingStyle', 'casual');
1517
+
1518
+ // Get spellcheck stats
1519
+ engine.executeCommand('getSpellcheckStats');
1520
+ // → { total, grammar, style, byRule, enabled, stylePreset, language, dictionarySize, ignoredCount }
1521
+
1522
+ // Style presets control which rules fire:
1523
+ // formal: passive voice + wordiness + cliches + punctuation (all rules)
1524
+ // casual: cliches + punctuation only (relaxed grammar)
1525
+ // technical: passive voice + punctuation (jargon OK, skip cliches)
1526
+ // academic: passive voice + wordiness + punctuation (citation-aware)
1527
+ ```
1528
+
1529
+ **Token types:** `comment`, `keyword`, `string`, `number`, `function`, `operator`, `punctuation`, `builtin`, `property`, `regex`, `decorator`, `type`, `tag`, `attr-name`, `attr-value`, `entity`.
1530
+
1531
+ **CollaborationPlugin** — Real-time collaborative editing with CRDT-based conflict resolution, live cursors, presence awareness, and configurable transport.
1532
+
1533
+ ```js
1534
+ import { CollaborationPlugin } from '@remyxjs/core';
1535
+
1536
+ engine.plugins.register(CollaborationPlugin({
1537
+ serverUrl: 'wss://signal.example.com',
1538
+ roomId: 'my-document-123',
1539
+ userName: 'Alice',
1540
+ userColor: '#6366f1',
1541
+ autoConnect: true, // connect on init (default: true)
1542
+ transport: 'websocket', // 'websocket' | 'webrtc' | custom transport
1543
+ offlineQueue: true, // queue changes while disconnected (default: true)
1544
+ awarenessTimeout: 30000, // ms before marking a peer as inactive
1545
+ onPeerJoined: (peer) => console.log(`${peer.name} joined`),
1546
+ onPeerLeft: (peer) => console.log(`${peer.name} left`),
1547
+ onSync: () => console.log('Document synced'),
1548
+ onError: (err) => console.error('Collaboration error:', err),
1549
+ }));
1550
+
1551
+ // Connect / disconnect manually (when autoConnect is false)
1552
+ engine.executeCommand('collaborationConnect', {
1553
+ serverUrl: 'wss://signal.example.com',
1554
+ roomId: 'room-1',
1555
+ userName: 'Bob',
1556
+ });
1557
+ engine.executeCommand('collaborationDisconnect');
1558
+
1559
+ // Query state
1560
+ engine.executeCommand('collaborationGetStatus');
1561
+ // → 'connected' | 'disconnected' | 'connecting' | 'error'
1562
+
1563
+ engine.executeCommand('collaborationGetPeers');
1564
+ // → [{ id, name, color, cursor, isActive }]
1565
+ ```
1566
+
1567
+ **Events:**
1568
+
1569
+ | Event | Payload | When |
1570
+ | --- | --- | --- |
1571
+ | `collaboration:connected` | `{ roomId, peerId }` | Successfully connected to room |
1572
+ | `collaboration:disconnected` | `{ reason }` | Disconnected from room |
1573
+ | `collaboration:peer-joined` | `{ peer }` | A new peer joined the room |
1574
+ | `collaboration:peer-left` | `{ peer }` | A peer left the room |
1575
+ | `collaboration:sync` | `{ documentState }` | Document state synchronized |
1576
+ | `collaboration:error` | `{ error, code }` | Connection or sync error |
1577
+
1578
+ **Custom transport interface:**
1579
+
1580
+ ```js
1581
+ const myTransport = {
1582
+ connect(roomId, handlers) {
1583
+ // handlers.onMessage(data) — call when receiving data
1584
+ // handlers.onConnect() — call when connected
1585
+ // handlers.onDisconnect(reason) — call when disconnected
1586
+ // Return a connection object with send(data) and close() methods
1587
+ return { send(data) { /* ... */ }, close() { /* ... */ } };
1588
+ },
1589
+ };
1590
+
1591
+ engine.plugins.register(CollaborationPlugin({
1592
+ transport: myTransport,
1593
+ roomId: 'room-1',
1594
+ userName: 'Charlie',
1595
+ }));
1596
+ ```
1597
+
1598
+ ## Selection API
1599
+
1600
+ Access the selection subsystem via `engine.selection`:
1601
+
1602
+ ```js
1603
+ const sel = engine.selection;
1604
+
1605
+ // Read selection state
1606
+ sel.isCollapsed(); // true if cursor (no range selected)
1607
+ sel.getSelectedText(); // selected plain text
1608
+ sel.getSelectedHTML(); // selected HTML fragment
1609
+ sel.getBoundingRect(); // DOMRect for positioning floating UI
1610
+
1611
+ // Inspect formatting at cursor
1612
+ const formats = sel.getActiveFormats();
1613
+ // {
1614
+ // bold: true, italic: false, underline: false,
1615
+ // strikethrough: false, subscript: false, superscript: false,
1616
+ // heading: 'h2',
1617
+ // alignment: 'left',
1618
+ // orderedList: false, unorderedList: true,
1619
+ // blockquote: false, codeBlock: false,
1620
+ // link: { href: 'https://...', text: '...', target: '_blank' },
1621
+ // fontFamily: 'Georgia', fontSize: '16px',
1622
+ // foreColor: '#333', backColor: null,
1623
+ // }
1624
+
1625
+ // Navigate the DOM
1626
+ sel.getParentElement(); // nearest element containing cursor
1627
+ sel.getParentBlock(); // nearest block element (div, p, li, etc.)
1628
+ sel.getClosestElement('a'); // find ancestor by tag name
1629
+
1630
+ // Manipulate content at cursor
1631
+ sel.insertHTML('<strong>injected</strong>');
1632
+ sel.insertNode(document.createElement('hr'));
1633
+ sel.wrapWith('mark', { class: 'highlight' });
1634
+ sel.unwrap('mark');
1635
+
1636
+ // Save and restore position (survives DOM mutations)
1637
+ const bookmark = sel.save();
1638
+ // ... modify DOM ...
1639
+ sel.restore(bookmark);
1640
+
1641
+ // Collapse cursor
1642
+ sel.collapse(); // collapse to start
1643
+ sel.collapse(true); // collapse to end
1644
+ ```
1645
+
1646
+ ## History (Undo/Redo)
1647
+
1648
+ Access the history subsystem via `engine.history`:
1649
+
1650
+ ```js
1651
+ engine.history.undo(); // Undo last change
1652
+ engine.history.redo(); // Redo
1653
+ engine.history.canUndo(); // true if undo stack is not empty
1654
+ engine.history.canRedo(); // true if redo stack is not empty
1655
+ engine.history.snapshot(); // Force an immediate snapshot
1656
+ engine.history.clear(); // Clear all history
1657
+
1658
+ // Events
1659
+ engine.on('history:undo', () => updateUndoButton());
1660
+ engine.on('history:redo', () => updateRedoButton());
1661
+ ```
1662
+
1663
+ History is configured via engine options:
1664
+
1665
+ ```js
1666
+ new EditorEngine(element, {
1667
+ history: {
1668
+ maxSize: 200, // Keep up to 200 undo states (default: 100)
1669
+ debounceMs: 500, // Wait 500ms of inactivity before snapshotting (default: 300)
1670
+ },
1671
+ });
1672
+ ```
1673
+
1674
+ The history system uses a `MutationObserver` to detect changes and debounces snapshots to avoid capturing every keystroke. When undoing/redoing, the observer is disconnected to prevent re-recording the restoration.
1675
+
1676
+ ## Keyboard Shortcuts
1677
+
1678
+ Access the keyboard subsystem via `engine.keyboard`:
1679
+
1680
+ ```js
1681
+ // Register custom shortcuts
1682
+ engine.keyboard.register('mod+shift+h', 'highlight');
1683
+ engine.keyboard.unregister('mod+shift+h');
1684
+
1685
+ // Look up shortcuts
1686
+ engine.keyboard.getShortcutForCommand('bold'); // 'mod+b'
1687
+ engine.keyboard.getShortcutLabel('mod+b'); // '⌘B' on Mac, 'Ctrl+B' on Windows
1688
+ ```
1689
+
1690
+ **Shortcut format:** Use `mod` for Ctrl (Windows/Linux) or Cmd (Mac). Combine with `shift`, `alt`, and a lowercase key: `'mod+shift+k'`.
1691
+
1692
+ **Default shortcuts:**
1693
+
1694
+ | Shortcut | Command |
1695
+ | --- | --- |
1696
+ | Mod+B | bold |
1697
+ | Mod+I | italic |
1698
+ | Mod+U | underline |
1699
+ | Mod+Shift+X | strikethrough |
1700
+ | Mod+, | subscript |
1701
+ | Mod+. | superscript |
1702
+ | Mod+K | insertLink |
1703
+ | Mod+Shift+7 | orderedList |
1704
+ | Mod+Shift+8 | unorderedList |
1705
+ | Mod+Shift+9 | blockquote |
1706
+ | Mod+Shift+C | codeBlock |
1707
+ | Mod+F | find |
1708
+ | Mod+Shift+U | sourceMode |
1709
+ | Mod+Shift+F | fullscreen |
1710
+ | Mod+Z | undo |
1711
+ | Mod+Shift+Z | redo |
1712
+ | Mod+Shift+P | commandPalette |
1713
+
1714
+ ## Sanitizer
1715
+
1716
+ The sanitizer runs on every content read (`getHTML()`) and write (`setHTML()`), and on pasted/dropped content.
1717
+
1718
+ ```js
1719
+ // Default behavior — blocks dangerous content automatically
1720
+ engine.getHTML(); // always sanitized
1721
+
1722
+ // Customize allowed tags, styles, and iframe domains via engine options
1723
+ new EditorEngine(element, {
1724
+ sanitize: {
1725
+ allowedTags: {
1726
+ // tag name → array of allowed attributes
1727
+ 'div': ['class', 'id', 'style'],
1728
+ 'span': ['class', 'style'],
1729
+ 'a': ['href', 'target', 'rel', 'class'],
1730
+ 'img': ['src', 'alt', 'width', 'height', 'class'],
1731
+ 'mark': ['class'],
1732
+ // ... see schema.js for full defaults
1733
+ },
1734
+ allowedStyles: [
1735
+ 'color', 'background-color', 'font-size', 'font-family',
1736
+ 'text-align', 'text-decoration', 'font-weight', 'font-style',
1737
+ 'margin', 'padding', 'border', 'width', 'height',
1738
+ // ... see schema.js for full defaults
1739
+ ],
1740
+ // Restrict which domains can be embedded via iframe
1741
+ iframeAllowedDomains: [
1742
+ 'www.youtube.com', 'youtube.com', 'www.youtube-nocookie.com',
1743
+ 'player.vimeo.com', 'www.dailymotion.com',
1744
+ // Add your own domains here
1745
+ ],
1746
+ },
1747
+ });
1748
+ ```
1749
+
1750
+ **Security protections:**
1751
+
1752
+ - Dangerous tags removed with children: `script`, `style`, `svg`, `math`, `form`, `object`, `embed`, `applet`, `template`
1753
+ - All `on*` event handler attributes blocked
1754
+ - `javascript:` and `vbscript:` URLs blocked
1755
+ - CSS injection blocked: `expression()`, `@import`, `behavior:`, `javascript:`
1756
+ - `<input>` restricted to `type="checkbox"` only
1757
+ - `<iframe>` src restricted to allowed domains only (YouTube, Vimeo, Dailymotion by default; HTTPS only)
1758
+ - `contenteditable` attribute stripped
1759
+ - SVG data URIs blocked in image sources
1760
+ - CSP-compatible: zero `document.execCommand` or `document.write` calls in source code
1761
+ - SRI hash support for `loadGoogleFonts()` to verify CDN asset integrity
1762
+
1763
+ ## Utilities
1764
+
1765
+ ### HTML Helpers
1766
+
1767
+ ```js
1768
+ import { escapeHTML, escapeHTMLAttr, insertPlainText } from '@remyxjs/core';
1769
+
1770
+ // Escape HTML entities for safe insertion
1771
+ escapeHTML('<script>alert("xss")</script>');
1772
+ // '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
1773
+
1774
+ // Escape for use in HTML attributes (also escapes quotes)
1775
+ escapeHTMLAttr('value with "quotes" & <brackets>');
1776
+ // 'value with &quot;quotes&quot; &amp; &lt;brackets&gt;'
1777
+
1778
+ // Insert plain text into the editor (handles paragraphs and line breaks)
1779
+ insertPlainText(engine, 'Line 1\n\nLine 2\nLine 3');
1780
+ // Inserts: <p>Line 1</p><p>Line 2<br>Line 3</p>
1781
+ ```
1782
+
1783
+ ### Markdown Conversion
1784
+
1785
+ ```js
1786
+ import { htmlToMarkdown, markdownToHtml } from '@remyxjs/core';
1787
+
1788
+ const md = htmlToMarkdown('<h1>Hello</h1><p>World</p>');
1789
+ // # Hello\n\nWorld
1790
+
1791
+ const html = markdownToHtml('# Hello\n\nWorld');
1792
+ // <h1>Hello</h1><p>World</p>
1793
+ ```
1794
+
1795
+ Supports GitHub Flavored Markdown (GFM): headings, bold, italic, links, images, lists, task lists, tables, code blocks (with language identifiers preserved), blockquotes, and horizontal rules.
1796
+
1797
+ ### Document Conversion
1798
+
1799
+ ```js
1800
+ import { convertDocument, isImportableFile, getSupportedExtensions } from '@remyxjs/core';
1801
+
1802
+ // Check if a file can be imported
1803
+ if (isImportableFile(file)) {
1804
+ const html = await convertDocument(file);
1805
+ engine.setHTML(html);
1806
+ }
1807
+
1808
+ // Supported extensions string (for file input accept attribute)
1809
+ const accept = getSupportedExtensions();
1810
+ // '.pdf,.docx,.md,.html,.htm,.txt,.csv,.tsv,.rtf'
1811
+ ```
1812
+
1813
+ **Supported formats:** PDF (requires `pdfjs-dist`), DOCX (requires `mammoth`), Markdown, HTML, TXT, CSV, TSV, RTF.
1814
+
1815
+ PDF and DOCX converters use dynamic imports — the heavy libraries are only loaded when a file of that type is imported.
1816
+
1817
+ ### Export
1818
+
1819
+ ```js
1820
+ import { exportAsMarkdown, exportAsPDF, exportAsDocx } from '@remyxjs/core';
1821
+
1822
+ // Export as Markdown file download
1823
+ exportAsMarkdown(engine.getHTML(), 'my-document');
1824
+
1825
+ // Export as PDF (opens print dialog)
1826
+ exportAsPDF(engine.getHTML(), 'My Document');
1827
+
1828
+ // Export as DOCX file download
1829
+ exportAsDocx(engine.getHTML(), 'my-document');
1830
+ ```
1831
+
1832
+ ### Paste Cleaning
1833
+
1834
+ ```js
1835
+ import { cleanPastedHTML, looksLikeMarkdown } from '@remyxjs/core';
1836
+
1837
+ // Remove source-specific markup (Word, Google Docs, LibreOffice, Apple Pages)
1838
+ const cleaned = cleanPastedHTML(rawHTML);
1839
+
1840
+ // Detect if pasted text is markdown
1841
+ if (looksLikeMarkdown(text)) {
1842
+ const html = markdownToHtml(text);
1843
+ }
1844
+ ```
1845
+
1846
+ The paste cleaner auto-detects the source application and applies format-specific cleanup: removing Word XML namespaces, Google Docs wrapper elements, LibreOffice meta tags, and Apple Pages-specific styling.
1847
+
1848
+ ### Font Management
1849
+
1850
+ ```js
1851
+ import { loadGoogleFonts, addFonts, removeFonts } from '@remyxjs/core';
1852
+
1853
+ // Load Google Fonts (injects <link> into <head>)
1854
+ loadGoogleFonts(['Roboto', 'Open Sans', 'Merriweather']);
1855
+
1856
+ // With Subresource Integrity (SRI) hash for security
1857
+ loadGoogleFonts(['Roboto'], {
1858
+ integrity: 'sha384-abc123...', // pre-computed hash of the stylesheet
1859
+ crossOrigin: 'anonymous', // required for SRI (default)
1860
+ });
1861
+
1862
+ // Modify a font list
1863
+ const fonts = ['Arial', 'Georgia', 'Times New Roman'];
1864
+ const updated = addFonts(fonts, ['Roboto', 'Lato'], { position: 0 });
1865
+ // ['Roboto', 'Lato', 'Arial', 'Georgia', 'Times New Roman']
1866
+
1867
+ const trimmed = removeFonts(updated, ['Times New Roman']);
1868
+ // ['Roboto', 'Lato', 'Arial', 'Georgia']
1869
+ ```
1870
+
1871
+ ### DOM Utilities
1872
+
1873
+ ```js
1874
+ import { closestBlock, closestTag, wrapInTag, unwrapTag, generateId, isBlockEmpty } from '@remyxjs/core';
1875
+
1876
+ closestBlock(node, editorElement); // Nearest block ancestor
1877
+ closestTag(node, 'a', editorElement); // Nearest <a> ancestor
1878
+ wrapInTag(range, 'mark', { class: 'hi' }); // Wrap range in element
1879
+ unwrapTag(markElement); // Unwrap, keep children
1880
+ generateId(); // 'rmx-a1b2c3d4'
1881
+ isBlockEmpty(paragraphElement); // true if no meaningful content
1882
+ ```
1883
+
1884
+ ### HTML Formatting
1885
+
1886
+ ```js
1887
+ import { formatHTML } from '@remyxjs/core';
1888
+
1889
+ const pretty = formatHTML('<div><p>Hello</p><p>World</p></div>');
1890
+ // <div>
1891
+ // <p>Hello</p>
1892
+ // <p>World</p>
1893
+ // </div>
1894
+ ```
1895
+
1896
+ ### Platform Detection
1897
+
1898
+ ```js
1899
+ import { isMac, getModKey } from '@remyxjs/core';
1900
+
1901
+ isMac(); // true on macOS
1902
+ getModKey(); // 'Cmd' on Mac, 'Ctrl' on Windows/Linux
1903
+ ```
1904
+
1905
+ ## Theming
1906
+
1907
+ ### Theme Variables
1908
+
1909
+ All styles use CSS custom properties with the `--rmx-` prefix. Override them in your CSS or use `createTheme` to generate overrides programmatically.
1910
+
1911
+ | Variable | Default (Light) | Description |
1912
+ | --- | --- | --- |
1913
+ | `--rmx-bg` | `#ffffff` | Editor background |
1914
+ | `--rmx-text` | `#1a1a1a` | Text color |
1915
+ | `--rmx-border` | `#e0e0e0` | Border color |
1916
+ | `--rmx-toolbar-bg` | `#f8f9fa` | Toolbar background |
1917
+ | `--rmx-toolbar-text` | `#374151` | Toolbar text color |
1918
+ | `--rmx-toolbar-hover` | `#e9ecef` | Toolbar button hover |
1919
+ | `--rmx-toolbar-active` | `#dee2e6` | Toolbar button active |
1920
+ | `--rmx-primary` | `#3b82f6` | Primary accent color |
1921
+ | `--rmx-primary-hover` | `#2563eb` | Primary hover |
1922
+ | `--rmx-primary-text` | `#ffffff` | Text on primary |
1923
+ | `--rmx-shadow` | `0 1px 3px ...` | Box shadow |
1924
+ | `--rmx-radius` | `6px` | Border radius |
1925
+ | `--rmx-font-family` | system-ui | Editor font |
1926
+ | `--rmx-font-size` | `16px` | Editor font size |
1927
+ | `--rmx-line-height` | `1.6` | Line height |
1928
+
1929
+ See `packages/remyx-core/src/themes/variables.css` for the full list of 40+ variables.
1930
+
1931
+ ### Built-in Themes
1932
+
1933
+ Six built-in themes are available via CSS classes (applied automatically by `@remyxjs/react`'s `theme` prop, or manually via `.rmx-theme-{name}`):
1934
+
1935
+ | Theme | Class | Description |
1936
+ | --- | --- | --- |
1937
+ | `light` | `.rmx-theme-light` | Clean white (default) |
1938
+ | `dark` | `.rmx-theme-dark` | Neutral dark |
1939
+ | `ocean` | `.rmx-theme-ocean` | Deep blue palette |
1940
+ | `forest` | `.rmx-theme-forest` | Green earth-tone palette |
1941
+ | `sunset` | `.rmx-theme-sunset` | Warm orange/amber palette |
1942
+ | `rose` | `.rmx-theme-rose` | Soft pink palette |
1943
+
1944
+ For vanilla JS usage, add the class to the editor wrapper:
1945
+
1946
+ ```js
1947
+ document.querySelector('.rmx-editor').classList.add('rmx-theme-ocean');
1948
+ ```
1949
+
1950
+ The `THEME_PRESETS` export is still available for programmatic overrides via `customTheme`:
1951
+
1952
+ ```js
1953
+ import { THEME_PRESETS, createTheme } from '@remyxjs/core';
1954
+ // Override a single variable on top of ocean
1955
+ const modified = { ...THEME_PRESETS.ocean, ...createTheme({ primary: '#ff6b6b' }) };
1956
+ ```
1957
+
1958
+ ### Custom Themes
1959
+
1960
+ ```js
1961
+ import { createTheme } from '@remyxjs/core';
1962
+
1963
+ // Use camelCase keys (automatically mapped to --rmx-* CSS vars)
1964
+ const theme = createTheme({
1965
+ bg: '#1e1e2e',
1966
+ text: '#cdd6f4',
1967
+ border: '#45475a',
1968
+ toolbarBg: '#181825',
1969
+ toolbarText: '#cdd6f4',
1970
+ primary: '#89b4fa',
1971
+ radius: '8px',
1972
+ });
1973
+
1974
+ // Apply to an editor wrapper element
1975
+ Object.entries(theme).forEach(([prop, value]) => {
1976
+ editorWrapper.style.setProperty(prop, value);
1977
+ });
1978
+ ```
1979
+
1980
+ You can also pass raw CSS variable names:
1981
+
1982
+ ```js
1983
+ const theme = createTheme({
1984
+ '--rmx-bg': '#1e1e2e',
1985
+ '--rmx-text': '#cdd6f4',
1986
+ });
1987
+ ```
1988
+
1989
+ ## Toolbar Configuration
1990
+
1991
+ ### Toolbar Presets
1992
+
1993
+ ```js
1994
+ import { TOOLBAR_PRESETS } from '@remyxjs/core';
1995
+
1996
+ // Available presets:
1997
+ TOOLBAR_PRESETS.full; // All available items including plugin commands
1998
+ TOOLBAR_PRESETS.rich; // All features with plugin toolbar items (callout, math, toc, bookmark, merge tag)
1999
+ TOOLBAR_PRESETS.standard; // Common editing features without plugins
2000
+ TOOLBAR_PRESETS.minimal; // Basic text formatting
2001
+ TOOLBAR_PRESETS.bare; // Bold, italic, underline only
2002
+ ```
2003
+
2004
+ Each preset is an array of arrays, where each inner array is a toolbar group (rendered with a separator between groups):
2005
+
2006
+ ```js
2007
+ [
2008
+ ['bold', 'italic', 'underline'], // Group 1
2009
+ ['heading', 'orderedList', 'unorderedList'], // Group 2
2010
+ ['link', 'image'], // Group 3
2011
+ ]
2012
+ ```
2013
+
2014
+ ### Custom Toolbars
2015
+
2016
+ ```js
2017
+ import { removeToolbarItems, addToolbarItems, createToolbar, TOOLBAR_PRESETS } from '@remyxjs/core';
2018
+
2019
+ // Start from a preset and customize
2020
+ let toolbar = TOOLBAR_PRESETS.standard;
2021
+
2022
+ // Remove items
2023
+ toolbar = removeToolbarItems(toolbar, ['strikethrough', 'subscript', 'superscript']);
2024
+
2025
+ // Add items to a specific group or position
2026
+ toolbar = addToolbarItems(toolbar, ['codeBlock', 'blockquote'], {
2027
+ position: 'after', // 'before' | 'after' | 'start' | 'end'
2028
+ relativeTo: 'link', // existing item to position relative to
2029
+ });
2030
+
2031
+ // Or build from scratch
2032
+ const myToolbar = createToolbar([
2033
+ ['bold', 'italic', 'underline'],
2034
+ ['heading'],
2035
+ ['orderedList', 'unorderedList'],
2036
+ ['link'],
2037
+ ['undo', 'redo'],
2038
+ ]);
2039
+ ```
2040
+
2041
+ ### Toolbar Item Theming
2042
+
2043
+ Style individual toolbar items differently:
2044
+
2045
+ ```js
2046
+ import { createToolbarItemTheme, resolveToolbarItemStyle } from '@remyxjs/core';
2047
+
2048
+ const itemTheme = createToolbarItemTheme({
2049
+ bold: {
2050
+ color: '#e74c3c',
2051
+ activeBackground: '#fce4ec',
2052
+ },
2053
+ heading: {
2054
+ fontSize: '14px',
2055
+ fontWeight: '600',
2056
+ },
2057
+ });
2058
+ ```
2059
+
2060
+ ## Configuration
2061
+
2062
+ ### defineConfig
2063
+
2064
+ Create reusable, named editor configurations:
2065
+
2066
+ ```js
2067
+ import { defineConfig } from '@remyxjs/core';
2068
+
2069
+ const config = defineConfig({
2070
+ // Shared defaults for all editors
2071
+ toolbar: TOOLBAR_PRESETS.standard,
2072
+ theme: THEME_PRESETS.ocean,
2073
+
2074
+ // Named editor variants
2075
+ editors: {
2076
+ blog: {
2077
+ toolbar: TOOLBAR_PRESETS.full,
2078
+ outputFormat: 'html',
2079
+ },
2080
+ comments: {
2081
+ toolbar: TOOLBAR_PRESETS.minimal,
2082
+ outputFormat: 'markdown',
2083
+ history: { maxSize: 50 },
2084
+ },
2085
+ },
2086
+ });
2087
+ ```
2088
+
2089
+ ### loadConfig
2090
+
2091
+ Load editor configuration from an external JSON or YAML URL:
2092
+
2093
+ ```js
2094
+ import { loadConfig } from '@remyxjs/core';
2095
+
2096
+ // JSON config
2097
+ const config = await loadConfig('https://cdn.example.com/editor-config.json');
2098
+
2099
+ // YAML config (detected by .yml/.yaml extension)
2100
+ const config = await loadConfig('/configs/editor.yaml');
2101
+
2102
+ // Environment-based merging
2103
+ const config = await loadConfig('/config.json', { env: 'production' });
2104
+
2105
+ // Custom headers (e.g., authenticated endpoints)
2106
+ const config = await loadConfig('/api/editor-config', {
2107
+ headers: { Authorization: 'Bearer token123' },
2108
+ });
2109
+
2110
+ // Cancellation
2111
+ const controller = new AbortController();
2112
+ const config = await loadConfig('/config.json', { signal: controller.signal });
2113
+ ```
2114
+
2115
+ **Config file format with environment overrides:**
2116
+
2117
+ ```json
2118
+ {
2119
+ "theme": "light",
2120
+ "height": 300,
2121
+ "toolbar": [["bold", "italic"], ["heading"], ["link"]],
2122
+ "menuBar": true,
2123
+ "env": {
2124
+ "production": {
2125
+ "height": 600,
2126
+ "readOnly": false
2127
+ },
2128
+ "development": {
2129
+ "height": 400
2130
+ }
2131
+ }
2132
+ }
2133
+ ```
2134
+
2135
+ When `env` is provided, the matching override is deep-merged onto the base config and the `env` key is stripped from the result. Arrays in env overrides replace (not merge) the base array.
2136
+
2137
+ **YAML support:**
2138
+
2139
+ ```yaml
2140
+ theme: ocean
2141
+ height: 500
2142
+ toolbar: [bold, italic, underline, heading, link]
2143
+ menuBar: true
2144
+ autosave:
2145
+ enabled: true
2146
+ interval: 30000
2147
+ ```
2148
+
2149
+ The built-in YAML parser handles simple key-value pairs, nested objects, inline arrays, booleans, numbers, null, and quoted strings. For complex YAML with anchors, multi-line blocks, or flow mappings, use the `js-yaml` library and pass the result to `defineConfig()`.
2150
+
2151
+ | Parameter | Type | Description |
2152
+ | --- | --- | --- |
2153
+ | `url` | `string` | URL or path to JSON/YAML config file |
2154
+ | `options.env` | `string` | Environment name for config merging |
2155
+ | `options.headers` | `Record<string, string>` | Custom fetch headers |
2156
+ | `options.signal` | `AbortSignal` | Cancellation signal |
2157
+
2158
+ ## Multi-Editor Support
2159
+
2160
+ When running multiple `EditorEngine` instances on a single page, two singletons help them work together efficiently.
2161
+
2162
+ ### EditorBus
2163
+
2164
+ A process-wide pub/sub bus for inter-editor communication. Use it to sync content between linked editors (e.g., source + preview), broadcast theme changes, or coordinate save operations.
2165
+
2166
+ ```js
2167
+ import { EditorBus } from '@remyxjs/core';
2168
+
2169
+ // Register editors so they can be looked up by ID
2170
+ EditorBus.register('source', sourceEngine);
2171
+ EditorBus.register('preview', previewEngine);
2172
+
2173
+ // Editor A broadcasts content on every change
2174
+ sourceEngine.on('content:change', () => {
2175
+ EditorBus.emit('sync:content', {
2176
+ id: 'source',
2177
+ html: sourceEngine.getHTML(),
2178
+ });
2179
+ });
2180
+
2181
+ // Editor B listens for updates
2182
+ EditorBus.on('sync:content', ({ id, html }) => {
2183
+ if (id !== 'preview') {
2184
+ previewEngine.setHTML(html);
2185
+ }
2186
+ });
2187
+
2188
+ // Broadcast a theme change into every editor's local event loop
2189
+ EditorBus.broadcast('theme:change', { theme: 'dark' });
2190
+
2191
+ // Broadcast to all except the sender
2192
+ EditorBus.broadcast('sync:content', { html }, { exclude: 'source' });
2193
+
2194
+ // Look up a registered editor
2195
+ const engine = EditorBus.getEditor('preview');
2196
+
2197
+ // List all registered IDs
2198
+ console.log(EditorBus.getEditorIds()); // ['source', 'preview']
2199
+
2200
+ // Unregister on destroy
2201
+ EditorBus.unregister('source');
2202
+ ```
2203
+
2204
+ | Method | Description |
2205
+ | --- | --- |
2206
+ | `register(id, engine)` | Register an engine by ID |
2207
+ | `unregister(id)` | Remove a registered engine |
2208
+ | `getEditor(id)` | Get an engine by ID |
2209
+ | `getEditorIds()` | List all registered IDs |
2210
+ | `editorCount` | Number of registered editors |
2211
+ | `on(event, handler)` | Subscribe to a global event |
2212
+ | `off(event, handler)` | Unsubscribe |
2213
+ | `once(event, handler)` | Subscribe once |
2214
+ | `emit(event, data)` | Emit to global subscribers |
2215
+ | `broadcast(event, data, opts)` | Emit into each editor's local `eventBus` |
2216
+ | `reset()` | Clear all listeners and registry (for tests) |
2217
+
2218
+ ### SharedResources
2219
+
2220
+ A lazily-initialized singleton that provides deeply-frozen copies of large, immutable data structures (sanitizer schema, toolbar presets, defaults, keybindings, command metadata). When running 10+ editors, all instances reference the same frozen objects instead of creating independent copies.
2221
+
2222
+ ```js
2223
+ import { SharedResources } from '@remyxjs/core';
2224
+
2225
+ // Shared, frozen sanitizer schema
2226
+ const { allowedTags, allowedStyles } = SharedResources.sanitizerSchema;
2227
+
2228
+ // Shared toolbar presets
2229
+ const fullToolbar = SharedResources.toolbarPresets.full;
2230
+
2231
+ // Shared defaults (toolbar, menuBar, fonts, fontSizes, colors, headingOptions)
2232
+ const defaultFonts = SharedResources.defaults.fonts;
2233
+
2234
+ // Shared keybinding table
2235
+ const keybindings = SharedResources.keybindings;
2236
+
2237
+ // Shared command metadata (buttons, tooltips, shortcuts, modals)
2238
+ const tooltips = SharedResources.commands.tooltips;
2239
+
2240
+ // Register a custom icon once, available to all editors
2241
+ SharedResources.registerIcon('myAction', '<svg viewBox="0 0 24 24">...</svg>');
2242
+ SharedResources.getIcon('myAction'); // '<svg ...>'
2243
+ SharedResources.getIconNames(); // ['myAction']
2244
+ SharedResources.unregisterIcon('myAction');
2245
+
2246
+ // Stats
2247
+ SharedResources.stats; // { registeredIcons: 0, frozenSchemas: true }
2248
+ ```
2249
+
2250
+ ## Constants
2251
+
2252
+ ```js
2253
+ import {
2254
+ // Toolbar & menu defaults
2255
+ DEFAULT_TOOLBAR, // Full toolbar configuration
2256
+ DEFAULT_MENU_BAR, // Menu bar structure
2257
+
2258
+ // Font & color defaults
2259
+ DEFAULT_FONTS, // Array of font family names
2260
+ DEFAULT_FONT_SIZES, // Array of size strings
2261
+ DEFAULT_COLORS, // Color palette array
2262
+
2263
+ // Keyboard
2264
+ DEFAULT_KEYBINDINGS, // Map: command name → shortcut string
2265
+
2266
+ // Heading options
2267
+ HEADING_OPTIONS, // H1-H6 + paragraph
2268
+
2269
+ // Security schema
2270
+ ALLOWED_TAGS, // Tag → allowed attributes map
2271
+ ALLOWED_STYLES, // Allowed CSS property names
2272
+
2273
+ // Command metadata
2274
+ BUTTON_COMMANDS, // Set of commands rendered as buttons
2275
+ TOOLTIP_MAP, // Command → tooltip text
2276
+ SHORTCUT_MAP, // Command → display shortcut
2277
+ MODAL_COMMANDS, // Commands that open modals
2278
+ getShortcutLabel, // (command) => platform-aware label string
2279
+ getCommandActiveState, // (command, selectionState, engine) => boolean
2280
+
2281
+ // Command palette
2282
+ SLASH_COMMAND_ITEMS, // Default catalog of command palette items
2283
+ filterSlashItems, // (items, query, options?) => filtered items (pinRecent: true by default)
2284
+ getRecentCommands, // () => string[] — last 5 executed command IDs
2285
+ recordRecentCommand, // (id) => void — record a command execution
2286
+ clearRecentCommands, // () => void — clear recent history
2287
+ registerCommandItems, // (items) => void — add custom items to the palette
2288
+ unregisterCommandItem, // (id) => boolean — remove a custom item
2289
+ getCustomCommandItems, // () => SlashCommandItem[] — get all custom items
2290
+
2291
+ // Comments & Annotations
2292
+ CommentsPlugin, // Inline comment threads plugin
2293
+ parseMentions, // (text) => string[] — extract @mentions from text
2294
+
2295
+ // Callouts & Alerts
2296
+ CalloutPlugin, // Styled callout blocks plugin
2297
+ registerCalloutType, // (typeDef) => void — register custom callout type
2298
+ unregisterCalloutType, // (type) => boolean — remove a callout type
2299
+ getCalloutTypes, // () => CalloutType[] — all registered types
2300
+ getCalloutType, // (type) => CalloutType — get a type definition
2301
+ parseGFMAlert, // (text) => { type, body } — parse GFM alert syntax
2302
+
2303
+ // Advanced Link Management
2304
+ LinkPlugin, // Link previews, broken links, auto-link, bookmarks
2305
+ detectLinks, // (text) => Array<{ type, value, index }>
2306
+ slugify, // (text) => URL-safe slug string
2307
+
2308
+ // Template System
2309
+ TemplatePlugin, // Merge tags, conditionals, loops, preview, library
2310
+ renderTemplate, // (template, data) => rendered string
2311
+ extractTags, // (template) => string[] — unique tag names
2312
+ registerTemplate, // Add custom template to library
2313
+ unregisterTemplate, // Remove template from library
2314
+ getTemplateLibrary, // () => all templates
2315
+ getTemplate, // (id) => template by ID
2316
+
2317
+ // Keyboard-First Editing
2318
+ KeyboardPlugin, // Vim/Emacs modes, auto-pair, multi-cursor
2319
+ getHeadings, // (element) => heading list with level/text/element
2320
+ selectNextOccurrence, // (element) => add next match to selection
2321
+
2322
+ // Drag & Drop
2323
+ DragDropPlugin, // Drop zones, cross-editor, file drops, reorder
2324
+
2325
+ // Math & Equations
2326
+ MathPlugin, // LaTeX math rendering, symbol palette, numbering
2327
+ getSymbolPalette, // () => categorized symbol array
2328
+ parseMathExpressions, // (text) => Array<{ type, src, index }>
2329
+ latexToMathML, // (latex) => MathML string
2330
+
2331
+ // Table of Contents
2332
+ TocPlugin, // Auto-generated TOC, outline, heading validation
2333
+ buildOutline, // (element) => hierarchical outline tree
2334
+ flattenOutline, // (outline) => flat item list
2335
+ renderTocHTML, // (outline) => HTML nav string
2336
+ validateHeadingHierarchy, // (flatItems) => warnings array
2337
+
2338
+ // Content Analytics
2339
+ AnalyticsPlugin, // Readability, reading time, SEO, goals
2340
+ analyzeContent, // (text, options) => comprehensive stats
2341
+ countSyllables, // (word) => number
2342
+ splitSentences, // (text) => string[]
2343
+ fleschKincaid, // (stats) => grade level
2344
+ fleschReadingEase, // (stats) => 0-100 score
2345
+ gunningFog, // (stats) => fog index
2346
+ colemanLiau, // (stats) => index
2347
+ vocabularyLevel, // (gradeLevel) => 'basic'|'intermediate'|'advanced'
2348
+ keywordDensity, // (text, keyword) => { count, density, positions }
2349
+ seoAnalysis, // (text, element, keyword) => { hints, ... }
2350
+
2351
+ // Spelling & Grammar
2352
+ SpellcheckPlugin, // Spellcheck + grammar checking with inline underlines
2353
+ analyzeGrammar, // (text, options) => issues array
2354
+ summarizeIssues, // (issues) => { total, grammar, style, byRule }
2355
+ detectPassiveVoice, // (text) => issues
2356
+ detectWordiness, // (text) => issues
2357
+ detectCliches, // (text) => issues
2358
+ detectPunctuationIssues, // (text) => issues
2359
+ STYLE_PRESETS, // { formal, casual, technical, academic }
2360
+
2361
+ // Real-time Collaboration
2362
+ CollaborationPlugin, // CRDT co-editing, live cursors, presence, transport
2363
+
2364
+ // Plugin registry
2365
+ registerPluginInRegistry, // Register a plugin for discovery
2366
+ unregisterPluginFromRegistry, // Remove from registry
2367
+ listRegisteredPlugins, // () => PluginRegistryEntry[] — all registered
2368
+ searchPluginRegistry, // (query) => PluginRegistryEntry[] — search by name/desc/tags
2369
+ } from '@remyxjs/core';
2370
+ ```
2371
+
2372
+ ## Tree-Shaking
2373
+
2374
+ `@remyxjs/core` is designed for tree-shaking. Import only what you need for the smallest possible bundle:
2375
+
2376
+ ```js
2377
+ // Minimal — only the engine and the commands you use
2378
+ import { EditorEngine, registerFormattingCommands, registerListCommands } from '@remyxjs/core';
2379
+ ```
2380
+
2381
+ ```js
2382
+ // Full — pulls in everything (larger bundle)
2383
+ import * as Remyx from '@remyxjs/core';
2384
+ ```
2385
+
2386
+ **Optional heavy dependencies:** `mammoth` (DOCX import) and `pdfjs-dist` (PDF import) are optional peer dependencies. Only install them if you need document import:
2387
+
2388
+ ```bash
2389
+ # Only if you need DOCX/PDF import
2390
+ npm install mammoth pdfjs-dist
2391
+ ```
2392
+
2393
+ **Theme modules are tree-shakeable:** Importing `createTheme` does not pull in `THEME_PRESETS` or the toolbar item theming utilities. These are separate modules that your bundler will exclude if unused.
2394
+
2395
+ ## CSS
2396
+
2397
+ Import the stylesheet for editor theming (light/dark modes, CSS custom properties):
2398
+
2399
+ ```js
2400
+ import '@remyxjs/core/style.css';
2401
+ ```
2402
+
2403
+ All styles use the `.rmx-` prefix and `--rmx-*` CSS custom properties. The stylesheet includes:
2404
+
2405
+ - **variables.css** — All CSS custom properties and their light-mode defaults
2406
+ - **light.css** — Light theme (default, auto-applied)
2407
+ - **dark.css** — Dark theme
2408
+ - **ocean.css** — Deep blue palette
2409
+ - **forest.css** — Green earth-tone palette
2410
+ - **sunset.css** — Warm orange/amber palette
2411
+ - **rose.css** — Soft pink palette
2412
+
2413
+ Each theme is a self-contained `.rmx-theme-{name}` class with complete variable overrides, content styles, code editor colors, and syntax token palettes. Apply a theme by adding the class to the editor wrapper or using `@remyxjs/react`'s `theme` prop.
2414
+
2415
+ ## Building Framework Wrappers
2416
+
2417
+ When creating a wrapper for a new framework, your package should:
2418
+
2419
+ 1. Depend on `@remyxjs/core` as a peer dependency
2420
+ 2. Import `EditorEngine` and register the commands you need
2421
+ 3. Create framework-native components for the toolbar, menu bar, modals, and status bar
2422
+ 4. Use `@remyxjs/core/style.css` for base theming and add component-specific CSS
2423
+ 5. Re-export `@remyxjs/core` for convenience so consumers don't need both packages
2424
+
2425
+ **Minimal Vue example:**
2426
+
2427
+ ```js
2428
+ // useRemyxEditor.js
2429
+ import { EditorEngine, registerFormattingCommands, registerListCommands } from '@remyxjs/core';
2430
+ import { ref, onMounted, onUnmounted } from 'vue';
2431
+
2432
+ export function useRemyxEditor(elementRef, options = {}) {
2433
+ const engine = ref(null);
2434
+
2435
+ onMounted(() => {
2436
+ engine.value = new EditorEngine(elementRef.value, options);
2437
+ registerFormattingCommands(engine.value);
2438
+ registerListCommands(engine.value);
2439
+ engine.value.init();
2440
+ });
2441
+
2442
+ onUnmounted(() => {
2443
+ engine.value?.destroy();
2444
+ });
2445
+
2446
+ return { engine };
2447
+ }
2448
+ ```
2449
+
2450
+ See [`@remyxjs/react`](../remyx-react/) as the full reference implementation.
2451
+
2452
+ ## License
2453
+
2454
+ MIT