@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.
- package/README.md +2454 -0
- package/dist/convertCsv-B8RVtdcs.cjs +2 -0
- package/dist/convertCsv-B8RVtdcs.cjs.map +1 -0
- package/dist/convertCsv-CKzZjzLJ.js +2 -0
- package/dist/convertCsv-CKzZjzLJ.js.map +1 -0
- package/dist/convertDocx-4q89XLLv.cjs +2 -0
- package/dist/convertDocx-4q89XLLv.cjs.map +1 -0
- package/dist/convertDocx-Dmx88twM.js +2 -0
- package/dist/convertDocx-Dmx88twM.js.map +1 -0
- package/dist/convertHtml-CtYVhiTh.js +2 -0
- package/dist/convertHtml-CtYVhiTh.js.map +1 -0
- package/dist/convertHtml-DbHrdrD3.cjs +2 -0
- package/dist/convertHtml-DbHrdrD3.cjs.map +1 -0
- package/dist/convertMarkdown-Di239Gtn.js +2 -0
- package/dist/convertMarkdown-Di239Gtn.js.map +1 -0
- package/dist/convertMarkdown-eJ9Nkoid.cjs +2 -0
- package/dist/convertMarkdown-eJ9Nkoid.cjs.map +1 -0
- package/dist/convertPdf-CFA1eNNH.js +2 -0
- package/dist/convertPdf-CFA1eNNH.js.map +1 -0
- package/dist/convertPdf-CSLmTrB8.cjs +2 -0
- package/dist/convertPdf-CSLmTrB8.cjs.map +1 -0
- package/dist/convertRtf-08CoScGD.js +2 -0
- package/dist/convertRtf-08CoScGD.js.map +1 -0
- package/dist/convertRtf-BfiBLMig.cjs +2 -0
- package/dist/convertRtf-BfiBLMig.cjs.map +1 -0
- package/dist/convertText-BpgzHRuh.cjs +2 -0
- package/dist/convertText-BpgzHRuh.cjs.map +1 -0
- package/dist/convertText-sa7PxKTe.js +2 -0
- package/dist/convertText-sa7PxKTe.js.map +1 -0
- package/dist/index-4syk9eEO.js +2 -0
- package/dist/index-4syk9eEO.js.map +1 -0
- package/dist/index-B25zSs0W.js +2 -0
- package/dist/index-B25zSs0W.js.map +1 -0
- package/dist/index-B7VT6ZLa.cjs +2 -0
- package/dist/index-B7VT6ZLa.cjs.map +1 -0
- package/dist/index-BCpytFKJ.js +2 -0
- package/dist/index-BCpytFKJ.js.map +1 -0
- package/dist/index-BNKANY5i.cjs +2 -0
- package/dist/index-BNKANY5i.cjs.map +1 -0
- package/dist/index-B_g_579T.cjs +2 -0
- package/dist/index-B_g_579T.cjs.map +1 -0
- package/dist/index-BvwyeoMb.js +3 -0
- package/dist/index-BvwyeoMb.js.map +1 -0
- package/dist/index-Bw7mlUQo.js +2 -0
- package/dist/index-Bw7mlUQo.js.map +1 -0
- package/dist/index-Byatzd-A.js +2 -0
- package/dist/index-Byatzd-A.js.map +1 -0
- package/dist/index-C0z9eZLm.cjs +2 -0
- package/dist/index-C0z9eZLm.cjs.map +1 -0
- package/dist/index-C88XPqjX.js +2 -0
- package/dist/index-C88XPqjX.js.map +1 -0
- package/dist/index-CI6FPF49.cjs +2 -0
- package/dist/index-CI6FPF49.cjs.map +1 -0
- package/dist/index-CLZF5_GB.cjs +2 -0
- package/dist/index-CLZF5_GB.cjs.map +1 -0
- package/dist/index-CXSwYlG4.cjs +2 -0
- package/dist/index-CXSwYlG4.cjs.map +1 -0
- package/dist/index-Ch9gotLk.js +2 -0
- package/dist/index-Ch9gotLk.js.map +1 -0
- package/dist/index-CifDpN1Y.js +2 -0
- package/dist/index-CifDpN1Y.js.map +1 -0
- package/dist/index-D5o8VpWJ.cjs +2 -0
- package/dist/index-D5o8VpWJ.cjs.map +1 -0
- package/dist/index-DKT1bABL.js +2 -0
- package/dist/index-DKT1bABL.js.map +1 -0
- package/dist/index-DWcn72PW.js +2 -0
- package/dist/index-DWcn72PW.js.map +1 -0
- package/dist/index-DjCGzPEv.cjs +2 -0
- package/dist/index-DjCGzPEv.cjs.map +1 -0
- package/dist/index-Dq0Jr1Ae.js +2 -0
- package/dist/index-Dq0Jr1Ae.js.map +1 -0
- package/dist/index-Dw0MVypb.cjs +2 -0
- package/dist/index-Dw0MVypb.cjs.map +1 -0
- package/dist/index-FEo3LShh.cjs +2 -0
- package/dist/index-FEo3LShh.cjs.map +1 -0
- package/dist/index-O1hzAUzi.cjs +2 -0
- package/dist/index-O1hzAUzi.cjs.map +1 -0
- package/dist/index-T1ZyLzeF.cjs +2 -0
- package/dist/index-T1ZyLzeF.cjs.map +1 -0
- package/dist/index-iRikoCdK.cjs +2 -0
- package/dist/index-iRikoCdK.cjs.map +1 -0
- package/dist/index-l6Yddj6x.js +2 -0
- package/dist/index-l6Yddj6x.js.map +1 -0
- package/dist/index-rD8LZENp.js +2 -0
- package/dist/index-rD8LZENp.js.map +1 -0
- package/dist/remyx-core.cjs +2 -0
- package/dist/remyx-core.cjs.map +1 -0
- package/dist/remyx-core.css +1 -0
- package/dist/remyx-core.js +2 -0
- package/dist/remyx-core.js.map +1 -0
- package/dist/themes/callouts.css +79 -0
- package/dist/themes/collaboration.css +117 -0
- package/dist/themes/comments.css +198 -0
- package/dist/themes/dark.css +109 -0
- package/dist/themes/forest.css +109 -0
- package/dist/themes/light.css +4 -0
- package/dist/themes/links.css +115 -0
- package/dist/themes/math-toc-analytics.css +129 -0
- package/dist/themes/ocean.css +109 -0
- package/dist/themes/rose.css +109 -0
- package/dist/themes/spellcheck.css +173 -0
- package/dist/themes/sunset.css +109 -0
- package/dist/themes/templates.css +87 -0
- package/dist/themes/variables.css +3222 -0
- package/package.json +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,2454 @@
|
|
|
1
|
+

|
|
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
|
+
// '<script>alert("xss")</script>'
|
|
1773
|
+
|
|
1774
|
+
// Escape for use in HTML attributes (also escapes quotes)
|
|
1775
|
+
escapeHTMLAttr('value with "quotes" & <brackets>');
|
|
1776
|
+
// 'value with "quotes" & <brackets>'
|
|
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
|