@rtif-sdk/engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cory Robinson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @rtif-sdk/engine
2
+
3
+ Editor engine for RTIF (Rich Text Input Format). Manages document state, dispatches operations through a plugin lifecycle, and provides undo/redo history.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @rtif-sdk/engine
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Create an engine
14
+
15
+ ```typescript
16
+ import { createEngine } from '@rtif-sdk/engine';
17
+ import type { Document } from '@rtif-sdk/core';
18
+
19
+ const doc: Document = {
20
+ version: 1,
21
+ blocks: [{ id: 'b1', type: 'text', spans: [{ text: '' }] }],
22
+ };
23
+
24
+ const engine = createEngine(doc);
25
+ ```
26
+
27
+ ### Dispatch operations
28
+
29
+ ```typescript
30
+ engine.dispatch({ type: 'insert_text', offset: 0, text: 'Hello' });
31
+
32
+ console.log(engine.state.doc.blocks[0].spans[0].text); // 'Hello'
33
+ ```
34
+
35
+ ### Undo / Redo
36
+
37
+ ```typescript
38
+ engine.undo(); // reverts to empty document
39
+ engine.redo(); // re-applies 'Hello'
40
+ ```
41
+
42
+ ### Subscribe to changes
43
+
44
+ ```typescript
45
+ const unsubscribe = engine.onChange((state) => {
46
+ console.log('Document changed:', state.doc);
47
+ });
48
+ ```
49
+
50
+ ### Plugins
51
+
52
+ ```typescript
53
+ import type { Plugin } from '@rtif-sdk/engine';
54
+
55
+ const myPlugin: Plugin = {
56
+ id: 'my-plugin',
57
+ init(engine) {
58
+ engine.registerCommand('greet', (eng) => {
59
+ eng.dispatch({ type: 'insert_text', offset: 0, text: 'Hi! ' });
60
+ });
61
+ },
62
+ };
63
+
64
+ engine.use(myPlugin);
65
+ engine.exec('greet');
66
+ ```
67
+
68
+ ## Plugin Lifecycle
69
+
70
+ 1. `beforeApply` hooks — inspect, modify, or cancel operations
71
+ 2. `apply()` — core applies operations to the document
72
+ 3. `afterApply` hooks — observe changes, trigger side effects
73
+ 4. `onChange` listeners — UI re-renders
74
+
75
+ Plugin errors are caught and isolated — a failing plugin never breaks the editor.
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Type-safe command name constants for built-in engine commands.
3
+ *
4
+ * Use these constants instead of raw strings when calling `engine.exec()`
5
+ * to get IDE autocomplete and catch typos at write-time.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { EngineCommands } from '@rtif-sdk/engine';
10
+ * engine.exec(EngineCommands.UNDO);
11
+ * engine.exec(EngineCommands.REDO);
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+ /**
17
+ * Command name constants for built-in engine commands (undo/redo).
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * engine.exec(EngineCommands.UNDO);
22
+ * engine.exec(EngineCommands.REDO);
23
+ * ```
24
+ */
25
+ export declare const EngineCommands: {
26
+ readonly UNDO: "undo";
27
+ readonly REDO: "redo";
28
+ };
29
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc;;;CAGjB,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Type-safe command name constants for built-in engine commands.
3
+ *
4
+ * Use these constants instead of raw strings when calling `engine.exec()`
5
+ * to get IDE autocomplete and catch typos at write-time.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { EngineCommands } from '@rtif-sdk/engine';
10
+ * engine.exec(EngineCommands.UNDO);
11
+ * engine.exec(EngineCommands.REDO);
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+ /**
17
+ * Command name constants for built-in engine commands (undo/redo).
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * engine.exec(EngineCommands.UNDO);
22
+ * engine.exec(EngineCommands.REDO);
23
+ * ```
24
+ */
25
+ export const EngineCommands = {
26
+ UNDO: 'undo',
27
+ REDO: 'redo',
28
+ };
29
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;CACJ,CAAC"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * EditorEngine — the state machine that drives all document mutations.
3
+ * See docs/spec/plugin-contract.md for specification.
4
+ */
5
+ import type { Document, Block, Selection, Operation, RangeMarksResult } from '@rtif-sdk/core';
6
+ import type { Plugin } from './plugin-host.js';
7
+ import type { ShortcutBinding } from './registries.js';
8
+ import type { PluginError } from './errors.js';
9
+ /** Runtime editor state */
10
+ export interface EditorState {
11
+ readonly doc: Document;
12
+ readonly selection: Selection;
13
+ /** Plugins can store state here, keyed by plugin ID */
14
+ readonly pluginState: Record<string, unknown>;
15
+ }
16
+ /** The engine interface — the only way to produce new states */
17
+ export interface IEditorEngine {
18
+ /** Current state (readonly from outside) */
19
+ readonly state: EditorState;
20
+ /** Dispatch an operation or a batch of operations */
21
+ dispatch(ops: Operation | Operation[]): void;
22
+ /** Dispatch a high-level command by name */
23
+ exec(command: string, payload?: unknown): void;
24
+ /**
25
+ * Check whether a command can execute in the current state.
26
+ * Returns true if the command exists and its canExecute returns true (or is not defined).
27
+ * Returns false if the command is not registered.
28
+ *
29
+ * @param command - The command name
30
+ * @param payload - Optional payload passed to canExecute
31
+ * @returns Whether the command can execute
32
+ */
33
+ canExecute(command: string, payload?: unknown): boolean;
34
+ /**
35
+ * Check whether a command's effect is currently active.
36
+ * Returns false if the command is not registered or has no isActive handler.
37
+ *
38
+ * @param command - The command name
39
+ * @param payload - Optional payload passed to isActive
40
+ * @returns Whether the command is active
41
+ */
42
+ isActive(command: string, payload?: unknown): boolean;
43
+ /** Undo the last operation group */
44
+ undo(): void;
45
+ /** Redo the last undone operation group */
46
+ redo(): void;
47
+ /** Register a plugin */
48
+ use(plugin: Plugin): void;
49
+ /** Subscribe to state changes */
50
+ onChange(listener: (state: EditorState) => void): () => void;
51
+ /**
52
+ * Subscribe to selection-only changes (cursor moves that don't modify the document).
53
+ * Fires when setSelection() is called with a different selection.
54
+ * Does NOT fire during dispatch/undo/redo (those fire onChange instead).
55
+ *
56
+ * @param listener - Function called with the new state after selection change
57
+ * @returns Unsubscribe function
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const unsub = engine.onSelectionChange((state) => {
62
+ * updateToolbar(state.selection);
63
+ * });
64
+ * ```
65
+ */
66
+ onSelectionChange(listener: (state: EditorState) => void): () => void;
67
+ /** Subscribe to plugin error events */
68
+ onError(listener: (error: PluginError) => void): () => void;
69
+ /**
70
+ * Update the engine's selection state without dispatching any operations.
71
+ *
72
+ * Used by platform implementations to sync cursor/selection changes that
73
+ * originate from user interaction (clicks, arrow keys, touch handles)
74
+ * back into the engine state, so that subsequent operations use the
75
+ * correct selection.
76
+ *
77
+ * Does NOT fire onChange listeners (the document did not change).
78
+ *
79
+ * @param selection - The new selection to store
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * // After reading a DOM selectionchange event
84
+ * engine.setSelection({ anchor: { offset: 5 }, focus: { offset: 5 } });
85
+ * ```
86
+ */
87
+ setSelection(selection: Selection): void;
88
+ /**
89
+ * Destroy the engine, calling destroy() on all plugins and cleaning up.
90
+ * After calling destroy(), no further operations can be dispatched.
91
+ */
92
+ destroy(): void;
93
+ /** Get the formatting marks at a specific document offset */
94
+ getMarksAtOffset(offset: number): Record<string, unknown>;
95
+ /** Get the common and mixed marks across a text range */
96
+ getMarksInRange(offset: number, count: number): RangeMarksResult;
97
+ /** Get the block containing a specific document offset */
98
+ getBlockAtOffset(offset: number): Block;
99
+ /** Get all registered keyboard shortcuts */
100
+ getShortcuts(): ReadonlyArray<ShortcutBinding>;
101
+ /**
102
+ * Set a pending mark that will be applied to the next inserted text.
103
+ * Pending marks are ephemeral — not part of EditorState, not serialized, not in undo history.
104
+ *
105
+ * @param mark - The mark key (e.g., 'bold')
106
+ * @param value - The mark value (e.g., true)
107
+ */
108
+ setPendingMark(mark: string, value: unknown): void;
109
+ /**
110
+ * Toggle a pending mark. If the mark is set, remove it. If not, set it to `value` (default: true).
111
+ *
112
+ * @param mark - The mark key
113
+ * @param value - The value to set if toggling on (default: true)
114
+ */
115
+ togglePendingMark(mark: string, value?: unknown): void;
116
+ /** Get a shallow copy of the current pending marks */
117
+ getPendingMarks(): Readonly<Record<string, unknown>>;
118
+ /** Clear all pending marks */
119
+ clearPendingMarks(): void;
120
+ }
121
+ /**
122
+ * Create a new EditorEngine with the given initial document.
123
+ *
124
+ * @param doc - The initial document
125
+ * @returns A new engine instance
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * const engine = createEngine({
130
+ * version: 1,
131
+ * blocks: [{ id: 'b1', type: 'text', spans: [{ text: '' }] }],
132
+ * });
133
+ * engine.dispatch({ type: 'insert_text', offset: 0, text: 'hello' });
134
+ * ```
135
+ */
136
+ export declare function createEngine(doc: Document): IEditorEngine;
137
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}