@rtif-sdk/web 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 +21 -0
- package/README.md +67 -0
- package/dist/block-drag-handler.d.ts +189 -0
- package/dist/block-drag-handler.d.ts.map +1 -0
- package/dist/block-drag-handler.js +745 -0
- package/dist/block-drag-handler.js.map +1 -0
- package/dist/block-renderer.d.ts +402 -0
- package/dist/block-renderer.d.ts.map +1 -0
- package/dist/block-renderer.js +424 -0
- package/dist/block-renderer.js.map +1 -0
- package/dist/clipboard.d.ts +178 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +432 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/command-bus.d.ts +113 -0
- package/dist/command-bus.d.ts.map +1 -0
- package/dist/command-bus.js +70 -0
- package/dist/command-bus.js.map +1 -0
- package/dist/composition.d.ts +220 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +271 -0
- package/dist/composition.js.map +1 -0
- package/dist/content-extraction.d.ts +69 -0
- package/dist/content-extraction.d.ts.map +1 -0
- package/dist/content-extraction.js +228 -0
- package/dist/content-extraction.js.map +1 -0
- package/dist/content-handler-file.d.ts +40 -0
- package/dist/content-handler-file.d.ts.map +1 -0
- package/dist/content-handler-file.js +91 -0
- package/dist/content-handler-file.js.map +1 -0
- package/dist/content-handler-image.d.ts +82 -0
- package/dist/content-handler-image.d.ts.map +1 -0
- package/dist/content-handler-image.js +120 -0
- package/dist/content-handler-image.js.map +1 -0
- package/dist/content-handler-url.d.ts +129 -0
- package/dist/content-handler-url.d.ts.map +1 -0
- package/dist/content-handler-url.js +244 -0
- package/dist/content-handler-url.js.map +1 -0
- package/dist/content-handlers.d.ts +67 -0
- package/dist/content-handlers.d.ts.map +1 -0
- package/dist/content-handlers.js +263 -0
- package/dist/content-handlers.js.map +1 -0
- package/dist/content-pipeline.d.ts +383 -0
- package/dist/content-pipeline.d.ts.map +1 -0
- package/dist/content-pipeline.js +232 -0
- package/dist/content-pipeline.js.map +1 -0
- package/dist/cursor-nav.d.ts +149 -0
- package/dist/cursor-nav.d.ts.map +1 -0
- package/dist/cursor-nav.js +230 -0
- package/dist/cursor-nav.js.map +1 -0
- package/dist/cursor-rect.d.ts +65 -0
- package/dist/cursor-rect.d.ts.map +1 -0
- package/dist/cursor-rect.js +98 -0
- package/dist/cursor-rect.js.map +1 -0
- package/dist/drop-indicator.d.ts +108 -0
- package/dist/drop-indicator.d.ts.map +1 -0
- package/dist/drop-indicator.js +236 -0
- package/dist/drop-indicator.js.map +1 -0
- package/dist/editor.d.ts +41 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +710 -0
- package/dist/editor.js.map +1 -0
- package/dist/floating-toolbar.d.ts +93 -0
- package/dist/floating-toolbar.d.ts.map +1 -0
- package/dist/floating-toolbar.js +159 -0
- package/dist/floating-toolbar.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/dist/input-bridge.d.ts +273 -0
- package/dist/input-bridge.d.ts.map +1 -0
- package/dist/input-bridge.js +884 -0
- package/dist/input-bridge.js.map +1 -0
- package/dist/link-popover.d.ts +38 -0
- package/dist/link-popover.d.ts.map +1 -0
- package/dist/link-popover.js +278 -0
- package/dist/link-popover.js.map +1 -0
- package/dist/mark-renderer.d.ts +275 -0
- package/dist/mark-renderer.d.ts.map +1 -0
- package/dist/mark-renderer.js +210 -0
- package/dist/mark-renderer.js.map +1 -0
- package/dist/perf.d.ts +145 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/perf.js +260 -0
- package/dist/perf.js.map +1 -0
- package/dist/plugin-kit.d.ts +265 -0
- package/dist/plugin-kit.d.ts.map +1 -0
- package/dist/plugin-kit.js +234 -0
- package/dist/plugin-kit.js.map +1 -0
- package/dist/plugins/alignment-plugin.d.ts +68 -0
- package/dist/plugins/alignment-plugin.d.ts.map +1 -0
- package/dist/plugins/alignment-plugin.js +98 -0
- package/dist/plugins/alignment-plugin.js.map +1 -0
- package/dist/plugins/block-utils.d.ts +113 -0
- package/dist/plugins/block-utils.d.ts.map +1 -0
- package/dist/plugins/block-utils.js +191 -0
- package/dist/plugins/block-utils.js.map +1 -0
- package/dist/plugins/blockquote-plugin.d.ts +39 -0
- package/dist/plugins/blockquote-plugin.d.ts.map +1 -0
- package/dist/plugins/blockquote-plugin.js +88 -0
- package/dist/plugins/blockquote-plugin.js.map +1 -0
- package/dist/plugins/bold-plugin.d.ts +37 -0
- package/dist/plugins/bold-plugin.d.ts.map +1 -0
- package/dist/plugins/bold-plugin.js +48 -0
- package/dist/plugins/bold-plugin.js.map +1 -0
- package/dist/plugins/callout-plugin.d.ts +100 -0
- package/dist/plugins/callout-plugin.d.ts.map +1 -0
- package/dist/plugins/callout-plugin.js +200 -0
- package/dist/plugins/callout-plugin.js.map +1 -0
- package/dist/plugins/code-block-plugin.d.ts +62 -0
- package/dist/plugins/code-block-plugin.d.ts.map +1 -0
- package/dist/plugins/code-block-plugin.js +176 -0
- package/dist/plugins/code-block-plugin.js.map +1 -0
- package/dist/plugins/code-plugin.d.ts +37 -0
- package/dist/plugins/code-plugin.d.ts.map +1 -0
- package/dist/plugins/code-plugin.js +48 -0
- package/dist/plugins/code-plugin.js.map +1 -0
- package/dist/plugins/embed-plugin.d.ts +90 -0
- package/dist/plugins/embed-plugin.d.ts.map +1 -0
- package/dist/plugins/embed-plugin.js +147 -0
- package/dist/plugins/embed-plugin.js.map +1 -0
- package/dist/plugins/font-family-plugin.d.ts +58 -0
- package/dist/plugins/font-family-plugin.d.ts.map +1 -0
- package/dist/plugins/font-family-plugin.js +57 -0
- package/dist/plugins/font-family-plugin.js.map +1 -0
- package/dist/plugins/font-size-plugin.d.ts +57 -0
- package/dist/plugins/font-size-plugin.d.ts.map +1 -0
- package/dist/plugins/font-size-plugin.js +56 -0
- package/dist/plugins/font-size-plugin.js.map +1 -0
- package/dist/plugins/heading-plugin.d.ts +52 -0
- package/dist/plugins/heading-plugin.d.ts.map +1 -0
- package/dist/plugins/heading-plugin.js +114 -0
- package/dist/plugins/heading-plugin.js.map +1 -0
- package/dist/plugins/hr-plugin.d.ts +33 -0
- package/dist/plugins/hr-plugin.d.ts.map +1 -0
- package/dist/plugins/hr-plugin.js +75 -0
- package/dist/plugins/hr-plugin.js.map +1 -0
- package/dist/plugins/image-plugin.d.ts +115 -0
- package/dist/plugins/image-plugin.d.ts.map +1 -0
- package/dist/plugins/image-plugin.js +199 -0
- package/dist/plugins/image-plugin.js.map +1 -0
- package/dist/plugins/indent-plugin.d.ts +62 -0
- package/dist/plugins/indent-plugin.d.ts.map +1 -0
- package/dist/plugins/indent-plugin.js +128 -0
- package/dist/plugins/indent-plugin.js.map +1 -0
- package/dist/plugins/index.d.ts +45 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +42 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/italic-plugin.d.ts +37 -0
- package/dist/plugins/italic-plugin.d.ts.map +1 -0
- package/dist/plugins/italic-plugin.js +48 -0
- package/dist/plugins/italic-plugin.js.map +1 -0
- package/dist/plugins/link-plugin.d.ts +129 -0
- package/dist/plugins/link-plugin.d.ts.map +1 -0
- package/dist/plugins/link-plugin.js +212 -0
- package/dist/plugins/link-plugin.js.map +1 -0
- package/dist/plugins/list-plugin.d.ts +53 -0
- package/dist/plugins/list-plugin.d.ts.map +1 -0
- package/dist/plugins/list-plugin.js +309 -0
- package/dist/plugins/list-plugin.js.map +1 -0
- package/dist/plugins/mark-utils.d.ts +173 -0
- package/dist/plugins/mark-utils.d.ts.map +1 -0
- package/dist/plugins/mark-utils.js +425 -0
- package/dist/plugins/mark-utils.js.map +1 -0
- package/dist/plugins/mention-plugin.d.ts +191 -0
- package/dist/plugins/mention-plugin.d.ts.map +1 -0
- package/dist/plugins/mention-plugin.js +295 -0
- package/dist/plugins/mention-plugin.js.map +1 -0
- package/dist/plugins/strikethrough-plugin.d.ts +37 -0
- package/dist/plugins/strikethrough-plugin.d.ts.map +1 -0
- package/dist/plugins/strikethrough-plugin.js +48 -0
- package/dist/plugins/strikethrough-plugin.js.map +1 -0
- package/dist/plugins/text-color-plugin.d.ts +57 -0
- package/dist/plugins/text-color-plugin.d.ts.map +1 -0
- package/dist/plugins/text-color-plugin.js +56 -0
- package/dist/plugins/text-color-plugin.js.map +1 -0
- package/dist/plugins/underline-plugin.d.ts +37 -0
- package/dist/plugins/underline-plugin.d.ts.map +1 -0
- package/dist/plugins/underline-plugin.js +48 -0
- package/dist/plugins/underline-plugin.js.map +1 -0
- package/dist/presets.d.ts +95 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +159 -0
- package/dist/presets.js.map +1 -0
- package/dist/renderer.d.ts +125 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +415 -0
- package/dist/renderer.js.map +1 -0
- package/dist/scroll-to-cursor.d.ts +25 -0
- package/dist/scroll-to-cursor.d.ts.map +1 -0
- package/dist/scroll-to-cursor.js +59 -0
- package/dist/scroll-to-cursor.js.map +1 -0
- package/dist/selection-sync.d.ts +159 -0
- package/dist/selection-sync.d.ts.map +1 -0
- package/dist/selection-sync.js +527 -0
- package/dist/selection-sync.js.map +1 -0
- package/dist/shortcut-handler.d.ts +98 -0
- package/dist/shortcut-handler.d.ts.map +1 -0
- package/dist/shortcut-handler.js +155 -0
- package/dist/shortcut-handler.js.map +1 -0
- package/dist/toolbar.d.ts +103 -0
- package/dist/toolbar.d.ts.map +1 -0
- package/dist/toolbar.js +134 -0
- package/dist/toolbar.js.map +1 -0
- package/dist/trigger-manager.d.ts +205 -0
- package/dist/trigger-manager.d.ts.map +1 -0
- package/dist/trigger-manager.js +466 -0
- package/dist/trigger-manager.js.map +1 -0
- package/dist/types.d.ts +216 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +30 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content pipeline — unified ingestion for paste, drop, and programmatic input.
|
|
3
|
+
*
|
|
4
|
+
* The content pipeline routes incoming content items through a priority-ordered
|
|
5
|
+
* chain of handlers. Each handler declares which MIME types it accepts and
|
|
6
|
+
* processes matching items into RTIF operations. All operations from a single
|
|
7
|
+
* process() call are batched into one engine.dispatch() for a single undo group.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
* @see docs/plans/web/03-content-pipeline.md
|
|
11
|
+
*/
|
|
12
|
+
import { deleteSelectionOps } from './clipboard.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// matchesMime helper
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Check whether a MIME type matches any of the given patterns.
|
|
18
|
+
*
|
|
19
|
+
* Supports exact match, wildcard subtypes (e.g., `image/*`), and
|
|
20
|
+
* catch-all (`*\/*`).
|
|
21
|
+
*
|
|
22
|
+
* @param contentType - The MIME type to test (e.g., 'text/plain')
|
|
23
|
+
* @param patterns - Array of MIME patterns to match against
|
|
24
|
+
* @returns true if the content type matches any pattern
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* matchesMime('image/png', ['image/*']); // true
|
|
29
|
+
* matchesMime('text/plain', ['text/html']); // false
|
|
30
|
+
* matchesMime('text/plain', ['*\/*']); // true (catch-all)
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function matchesMime(contentType, patterns) {
|
|
34
|
+
for (const pattern of patterns) {
|
|
35
|
+
// Catch-all
|
|
36
|
+
if (pattern === '*/*')
|
|
37
|
+
return true;
|
|
38
|
+
// Wildcard subtype: 'image/*' matches 'image/png'
|
|
39
|
+
if (pattern.endsWith('/*')) {
|
|
40
|
+
const prefix = pattern.slice(0, -1); // 'image/'
|
|
41
|
+
if (contentType.startsWith(prefix))
|
|
42
|
+
return true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Exact match
|
|
46
|
+
if (contentType === pattern)
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Factory
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/**
|
|
55
|
+
* Create a content pipeline instance.
|
|
56
|
+
*
|
|
57
|
+
* The pipeline manages a priority-ordered set of content handlers and routes
|
|
58
|
+
* incoming content items through them. All operations dispatched by handlers
|
|
59
|
+
* are buffered and flushed as a single engine.dispatch() call, ensuring each
|
|
60
|
+
* paste/drop is a single undo group.
|
|
61
|
+
*
|
|
62
|
+
* @returns A new ContentPipeline instance
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* const pipeline = createContentPipeline();
|
|
67
|
+
* pipeline.register(createPlainTextHandler(() => generateId()));
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function createContentPipeline() {
|
|
71
|
+
let handlers = [];
|
|
72
|
+
const eventListeners = new Set();
|
|
73
|
+
let destroyed = false;
|
|
74
|
+
/**
|
|
75
|
+
* Get handlers sorted by descending priority (higher runs first).
|
|
76
|
+
*/
|
|
77
|
+
function getSortedHandlers() {
|
|
78
|
+
return [...handlers].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Emit a pipeline event to all listeners.
|
|
82
|
+
*/
|
|
83
|
+
function emit(event) {
|
|
84
|
+
for (const listener of eventListeners) {
|
|
85
|
+
try {
|
|
86
|
+
listener(event);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Event listener errors must not break the pipeline
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a ContentContext that buffers operations for a single process() call.
|
|
95
|
+
*/
|
|
96
|
+
function createContext(opts) {
|
|
97
|
+
const opsBuffer = [];
|
|
98
|
+
let selectionDeleted = false;
|
|
99
|
+
// Compute insert offset: dropOffset takes precedence, otherwise use selection
|
|
100
|
+
const sel = opts.engine.state.selection;
|
|
101
|
+
const insertOffset = opts.dropOffset !== undefined
|
|
102
|
+
? opts.dropOffset
|
|
103
|
+
: Math.min(sel.anchor.offset, sel.focus.offset);
|
|
104
|
+
const context = {
|
|
105
|
+
engine: opts.engine,
|
|
106
|
+
commands: opts.commands,
|
|
107
|
+
insertOffset,
|
|
108
|
+
source: opts.source,
|
|
109
|
+
deleteSelectionAndGetOffset() {
|
|
110
|
+
if (selectionDeleted)
|
|
111
|
+
return insertOffset;
|
|
112
|
+
selectionDeleted = true;
|
|
113
|
+
const currentSel = opts.engine.state.selection;
|
|
114
|
+
const doc = opts.engine.state.doc;
|
|
115
|
+
const deleteOps = deleteSelectionOps(doc, currentSel);
|
|
116
|
+
if (deleteOps.length > 0) {
|
|
117
|
+
opsBuffer.push(...deleteOps);
|
|
118
|
+
}
|
|
119
|
+
return insertOffset;
|
|
120
|
+
},
|
|
121
|
+
dispatch(ops) {
|
|
122
|
+
opsBuffer.push(...ops);
|
|
123
|
+
},
|
|
124
|
+
generateBlockId() {
|
|
125
|
+
return opts.generateBlockId();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
return { context, getBufferedOps: () => opsBuffer };
|
|
129
|
+
}
|
|
130
|
+
const pipeline = {
|
|
131
|
+
register(handler) {
|
|
132
|
+
if (destroyed) {
|
|
133
|
+
return { dispose: () => { } };
|
|
134
|
+
}
|
|
135
|
+
handlers.push(handler);
|
|
136
|
+
return {
|
|
137
|
+
dispose() {
|
|
138
|
+
handlers = handlers.filter((h) => h !== handler);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
async process(items, contextOpts) {
|
|
143
|
+
if (destroyed || items.length === 0)
|
|
144
|
+
return false;
|
|
145
|
+
// Emit contentReceived event (allows cancellation)
|
|
146
|
+
const receivedEvent = {
|
|
147
|
+
type: 'contentReceived',
|
|
148
|
+
items,
|
|
149
|
+
source: contextOpts.source,
|
|
150
|
+
prevented: false,
|
|
151
|
+
preventDefault() {
|
|
152
|
+
this.prevented = true;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
emit(receivedEvent);
|
|
156
|
+
if (receivedEvent.prevented)
|
|
157
|
+
return false;
|
|
158
|
+
const sorted = getSortedHandlers();
|
|
159
|
+
const { context, getBufferedOps } = createContext(contextOpts);
|
|
160
|
+
// Handler-first iteration: for each handler, check each item
|
|
161
|
+
for (const handler of sorted) {
|
|
162
|
+
for (const item of items) {
|
|
163
|
+
// Check MIME type match
|
|
164
|
+
if (!matchesMime(item.type, handler.accept))
|
|
165
|
+
continue;
|
|
166
|
+
// Optional canHandle check
|
|
167
|
+
if (handler.canHandle) {
|
|
168
|
+
try {
|
|
169
|
+
const can = await handler.canHandle(item, context);
|
|
170
|
+
if (!can)
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// canHandle errors skip this handler
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Try to handle
|
|
179
|
+
try {
|
|
180
|
+
const handled = await handler.handle(item, context);
|
|
181
|
+
if (handled) {
|
|
182
|
+
// Flush all buffered ops as a single dispatch
|
|
183
|
+
const allOps = getBufferedOps();
|
|
184
|
+
if (allOps.length > 0) {
|
|
185
|
+
contextOpts.engine.dispatch(allOps);
|
|
186
|
+
}
|
|
187
|
+
// Emit contentHandled
|
|
188
|
+
emit({
|
|
189
|
+
type: 'contentHandled',
|
|
190
|
+
handlerId: handler.id,
|
|
191
|
+
item,
|
|
192
|
+
});
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// handler.handle() errors skip this handler+item pair
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// No handler accepted any item — emit contentRejected for each
|
|
203
|
+
for (const item of items) {
|
|
204
|
+
const hasMatchingHandler = sorted.some((h) => matchesMime(item.type, h.accept));
|
|
205
|
+
emit({
|
|
206
|
+
type: 'contentRejected',
|
|
207
|
+
item,
|
|
208
|
+
reason: hasMatchingHandler ? 'all-declined' : 'no-handler',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
},
|
|
213
|
+
hasHandlerForType(type) {
|
|
214
|
+
if (destroyed)
|
|
215
|
+
return false;
|
|
216
|
+
return handlers.some((h) => matchesMime(type, h.accept));
|
|
217
|
+
},
|
|
218
|
+
onEvent(listener) {
|
|
219
|
+
eventListeners.add(listener);
|
|
220
|
+
return () => {
|
|
221
|
+
eventListeners.delete(listener);
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
destroy() {
|
|
225
|
+
destroyed = true;
|
|
226
|
+
handlers = [];
|
|
227
|
+
eventListeners.clear();
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
return pipeline;
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=content-pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-pipeline.js","sourceRoot":"","sources":["../src/content-pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard navigation handler for the RTIF web editor.
|
|
3
|
+
*
|
|
4
|
+
* Handles `keydown` events for:
|
|
5
|
+
* - Undo / redo (Cmd+Z, Cmd+Shift+Z / Ctrl+Z, Ctrl+Y)
|
|
6
|
+
* - Select all (Cmd+A / Ctrl+A)
|
|
7
|
+
* - Document-level cursor movement (Cmd+Home/End, Cmd+Up/Down)
|
|
8
|
+
* - Document-level selection extension (Cmd+Shift+Home/End, Cmd+Shift+Up/Down)
|
|
9
|
+
*
|
|
10
|
+
* Arrow keys, word-level movement, and Home/End within a line are delegated
|
|
11
|
+
* to the browser's native `contenteditable` behavior. The `selectionchange`
|
|
12
|
+
* event (handled by selection-sync) reads the resulting DOM selection back
|
|
13
|
+
* into RTIF.
|
|
14
|
+
*
|
|
15
|
+
* @see docs/spec/platform-requirements.md sections 2, 3, and 5
|
|
16
|
+
*/
|
|
17
|
+
import type { Document, Selection, Operation } from '@rtif-sdk/core';
|
|
18
|
+
/**
|
|
19
|
+
* Detect whether the current platform is macOS or iOS.
|
|
20
|
+
*
|
|
21
|
+
* Uses `navigator.platform` with a fallback for iPad Safari (which reports
|
|
22
|
+
* `"MacIntel"` but has touch support).
|
|
23
|
+
*
|
|
24
|
+
* @returns `true` on Apple platforms, `false` otherwise
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* if (isMac()) {
|
|
29
|
+
* // Use metaKey for keyboard shortcuts
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function isMac(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Check whether the platform modifier key is pressed.
|
|
36
|
+
*
|
|
37
|
+
* On macOS/iOS this is the Meta (Command) key; on other platforms it is
|
|
38
|
+
* the Control key.
|
|
39
|
+
*
|
|
40
|
+
* @param e - The keyboard event to inspect
|
|
41
|
+
* @returns `true` if the platform modifier key is held
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* element.addEventListener('keydown', (e) => {
|
|
46
|
+
* if (isModKey(e) && e.key === 'z') {
|
|
47
|
+
* // undo
|
|
48
|
+
* }
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function isModKey(e: KeyboardEvent): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Dependencies injected into {@link CursorNavHandler}.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const deps: CursorNavDeps = {
|
|
59
|
+
* getSelection: () => engine.state.selection,
|
|
60
|
+
* getDoc: () => engine.state.doc,
|
|
61
|
+
* dispatch: (ops) => engine.dispatch(ops),
|
|
62
|
+
* undo: () => engine.undo(),
|
|
63
|
+
* redo: () => engine.redo(),
|
|
64
|
+
* isComposing: () => compositionHandler.isComposing(),
|
|
65
|
+
* isReadOnly: () => config.readOnly ?? false,
|
|
66
|
+
* setSelection: (sel) => { ... },
|
|
67
|
+
* isMac: () => isMac(),
|
|
68
|
+
* };
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export interface CursorNavDeps {
|
|
72
|
+
/** Return the current RTIF selection. */
|
|
73
|
+
readonly getSelection: () => Selection;
|
|
74
|
+
/** Return the current RTIF document. */
|
|
75
|
+
readonly getDoc: () => Document;
|
|
76
|
+
/** Dispatch one or more RTIF operations to the engine. */
|
|
77
|
+
readonly dispatch: (ops: Operation | Operation[]) => void;
|
|
78
|
+
/** Trigger undo on the engine. */
|
|
79
|
+
readonly undo: () => void;
|
|
80
|
+
/** Trigger redo on the engine. */
|
|
81
|
+
readonly redo: () => void;
|
|
82
|
+
/** Whether an IME composition session is active. */
|
|
83
|
+
readonly isComposing: () => boolean;
|
|
84
|
+
/** Whether the editor is in read-only mode. */
|
|
85
|
+
readonly isReadOnly: () => boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Update the RTIF selection without dispatching an operation.
|
|
88
|
+
* This triggers DOM selection sync and onChange notification.
|
|
89
|
+
*/
|
|
90
|
+
readonly setSelection: (selection: Selection) => void;
|
|
91
|
+
/** Whether the current platform is macOS/iOS. */
|
|
92
|
+
readonly isMac: () => boolean;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Handles `keydown` events for keyboard shortcuts and document-level
|
|
96
|
+
* cursor navigation.
|
|
97
|
+
*
|
|
98
|
+
* The handler intercepts:
|
|
99
|
+
* - **Undo/redo**: Cmd+Z, Cmd+Shift+Z (Mac) / Ctrl+Z, Ctrl+Y (Win/Linux)
|
|
100
|
+
* - **Select all**: Cmd+A / Ctrl+A
|
|
101
|
+
* - **Document start/end**: Cmd+Home, Cmd+End, Cmd+Up, Cmd+Down (Mac)
|
|
102
|
+
* - **Selection to document edges**: Cmd+Shift+Home/End/Up/Down
|
|
103
|
+
*
|
|
104
|
+
* All other key events (including arrow keys, Home/End without Cmd,
|
|
105
|
+
* word-level movement) are left to the browser's native contenteditable
|
|
106
|
+
* handling. The selection-sync module reads the resulting DOM selection
|
|
107
|
+
* back into RTIF via `selectionchange`.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* const handler = new CursorNavHandler(root, deps);
|
|
112
|
+
* handler.attach();
|
|
113
|
+
* // ... later ...
|
|
114
|
+
* handler.detach();
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export declare class CursorNavHandler {
|
|
118
|
+
private readonly _root;
|
|
119
|
+
private readonly _deps;
|
|
120
|
+
private _keydownHandler;
|
|
121
|
+
constructor(root: HTMLElement, deps: CursorNavDeps);
|
|
122
|
+
/**
|
|
123
|
+
* Attach the `keydown` event listener to the root element.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* handler.attach();
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
attach(): void;
|
|
131
|
+
/**
|
|
132
|
+
* Remove the `keydown` event listener from the root element.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* handler.detach();
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
detach(): void;
|
|
140
|
+
private _onKeydown;
|
|
141
|
+
/**
|
|
142
|
+
* Handle Cmd/Ctrl + Home/End/ArrowUp/ArrowDown for document-level navigation.
|
|
143
|
+
*
|
|
144
|
+
* Returns `true` if the event was handled (and preventDefault was called),
|
|
145
|
+
* `false` if the event should be left to the browser.
|
|
146
|
+
*/
|
|
147
|
+
private _handleDocumentNav;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=cursor-nav.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-nav.d.ts","sourceRoot":"","sources":["../src/cursor-nav.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAOrE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,IAAI,OAAO,CAM/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,aAAa,GAAG,OAAO,CAElD;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,QAAQ,CAAC,YAAY,EAAE,MAAM,SAAS,CAAC;IAEvC,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,QAAQ,CAAC;IAEhC,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,SAAS,GAAG,SAAS,EAAE,KAAK,IAAI,CAAC;IAE1D,kCAAkC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAE1B,kCAAkC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAE1B,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC;IAEpC,+CAA+C;IAC/C,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC;IAEnC;;;OAGG;IACH,QAAQ,CAAC,YAAY,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC;IAEtD,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC;CAC/B;AAMD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,eAAe,CAA6C;gBAExD,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa;IAKlD;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;IAKd;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;IAWd,OAAO,CAAC,UAAU;IAuDlB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;CA6C3B"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard navigation handler for the RTIF web editor.
|
|
3
|
+
*
|
|
4
|
+
* Handles `keydown` events for:
|
|
5
|
+
* - Undo / redo (Cmd+Z, Cmd+Shift+Z / Ctrl+Z, Ctrl+Y)
|
|
6
|
+
* - Select all (Cmd+A / Ctrl+A)
|
|
7
|
+
* - Document-level cursor movement (Cmd+Home/End, Cmd+Up/Down)
|
|
8
|
+
* - Document-level selection extension (Cmd+Shift+Home/End, Cmd+Shift+Up/Down)
|
|
9
|
+
*
|
|
10
|
+
* Arrow keys, word-level movement, and Home/End within a line are delegated
|
|
11
|
+
* to the browser's native `contenteditable` behavior. The `selectionchange`
|
|
12
|
+
* event (handled by selection-sync) reads the resulting DOM selection back
|
|
13
|
+
* into RTIF.
|
|
14
|
+
*
|
|
15
|
+
* @see docs/spec/platform-requirements.md sections 2, 3, and 5
|
|
16
|
+
*/
|
|
17
|
+
import { docLength } from '@rtif-sdk/core';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Platform detection
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Detect whether the current platform is macOS or iOS.
|
|
23
|
+
*
|
|
24
|
+
* Uses `navigator.platform` with a fallback for iPad Safari (which reports
|
|
25
|
+
* `"MacIntel"` but has touch support).
|
|
26
|
+
*
|
|
27
|
+
* @returns `true` on Apple platforms, `false` otherwise
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* if (isMac()) {
|
|
32
|
+
* // Use metaKey for keyboard shortcuts
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function isMac() {
|
|
37
|
+
if (typeof navigator === 'undefined')
|
|
38
|
+
return false;
|
|
39
|
+
return /Mac|iPod|iPhone|iPad/.test(navigator.platform)
|
|
40
|
+
|| (navigator.platform === 'MacIntel'
|
|
41
|
+
&& typeof navigator.maxTouchPoints === 'number'
|
|
42
|
+
&& navigator.maxTouchPoints > 1);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check whether the platform modifier key is pressed.
|
|
46
|
+
*
|
|
47
|
+
* On macOS/iOS this is the Meta (Command) key; on other platforms it is
|
|
48
|
+
* the Control key.
|
|
49
|
+
*
|
|
50
|
+
* @param e - The keyboard event to inspect
|
|
51
|
+
* @returns `true` if the platform modifier key is held
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* element.addEventListener('keydown', (e) => {
|
|
56
|
+
* if (isModKey(e) && e.key === 'z') {
|
|
57
|
+
* // undo
|
|
58
|
+
* }
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function isModKey(e) {
|
|
63
|
+
return isMac() ? e.metaKey : e.ctrlKey;
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// CursorNavHandler class
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Handles `keydown` events for keyboard shortcuts and document-level
|
|
70
|
+
* cursor navigation.
|
|
71
|
+
*
|
|
72
|
+
* The handler intercepts:
|
|
73
|
+
* - **Undo/redo**: Cmd+Z, Cmd+Shift+Z (Mac) / Ctrl+Z, Ctrl+Y (Win/Linux)
|
|
74
|
+
* - **Select all**: Cmd+A / Ctrl+A
|
|
75
|
+
* - **Document start/end**: Cmd+Home, Cmd+End, Cmd+Up, Cmd+Down (Mac)
|
|
76
|
+
* - **Selection to document edges**: Cmd+Shift+Home/End/Up/Down
|
|
77
|
+
*
|
|
78
|
+
* All other key events (including arrow keys, Home/End without Cmd,
|
|
79
|
+
* word-level movement) are left to the browser's native contenteditable
|
|
80
|
+
* handling. The selection-sync module reads the resulting DOM selection
|
|
81
|
+
* back into RTIF via `selectionchange`.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const handler = new CursorNavHandler(root, deps);
|
|
86
|
+
* handler.attach();
|
|
87
|
+
* // ... later ...
|
|
88
|
+
* handler.detach();
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export class CursorNavHandler {
|
|
92
|
+
_root;
|
|
93
|
+
_deps;
|
|
94
|
+
_keydownHandler = null;
|
|
95
|
+
constructor(root, deps) {
|
|
96
|
+
this._root = root;
|
|
97
|
+
this._deps = deps;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Attach the `keydown` event listener to the root element.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* handler.attach();
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
attach() {
|
|
108
|
+
this._keydownHandler = (e) => this._onKeydown(e);
|
|
109
|
+
this._root.addEventListener('keydown', this._keydownHandler);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Remove the `keydown` event listener from the root element.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* handler.detach();
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
detach() {
|
|
120
|
+
if (this._keydownHandler) {
|
|
121
|
+
this._root.removeEventListener('keydown', this._keydownHandler);
|
|
122
|
+
this._keydownHandler = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// -----------------------------------------------------------------------
|
|
126
|
+
// Keydown dispatcher
|
|
127
|
+
// -----------------------------------------------------------------------
|
|
128
|
+
_onKeydown(e) {
|
|
129
|
+
// Never interfere with IME composition
|
|
130
|
+
if (this._deps.isComposing())
|
|
131
|
+
return;
|
|
132
|
+
const mod = this._deps.isMac() ? e.metaKey : e.ctrlKey;
|
|
133
|
+
// --- Undo: Cmd+Z / Ctrl+Z (no shift, no alt) ---
|
|
134
|
+
if (mod && !e.shiftKey && !e.altKey && e.key === 'z') {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
this._deps.undo();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// --- Redo: Cmd+Shift+Z (Mac) / Ctrl+Y (Win/Linux) ---
|
|
140
|
+
if (this._deps.isMac()) {
|
|
141
|
+
if (e.metaKey && e.shiftKey && !e.altKey && e.key === 'z') {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
this._deps.redo();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
if (e.ctrlKey && !e.shiftKey && !e.altKey && e.key === 'y') {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
this._deps.redo();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Also support Ctrl+Shift+Z on non-Mac
|
|
154
|
+
if (e.ctrlKey && e.shiftKey && !e.altKey && e.key === 'z') {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
this._deps.redo();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// --- Select all: Cmd+A / Ctrl+A ---
|
|
161
|
+
if (mod && !e.shiftKey && !e.altKey && e.key === 'a') {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
const doc = this._deps.getDoc();
|
|
164
|
+
const len = docLength(doc);
|
|
165
|
+
this._deps.setSelection({
|
|
166
|
+
anchor: { offset: 0 },
|
|
167
|
+
focus: { offset: len },
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// --- Document-level navigation ---
|
|
172
|
+
// These handle Cmd+Home/End and Cmd+Up/Down (Mac equivalents).
|
|
173
|
+
// With Shift held, they extend selection instead of moving cursor.
|
|
174
|
+
if (mod && !e.altKey) {
|
|
175
|
+
if (this._handleDocumentNav(e))
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Handle Cmd/Ctrl + Home/End/ArrowUp/ArrowDown for document-level navigation.
|
|
181
|
+
*
|
|
182
|
+
* Returns `true` if the event was handled (and preventDefault was called),
|
|
183
|
+
* `false` if the event should be left to the browser.
|
|
184
|
+
*/
|
|
185
|
+
_handleDocumentNav(e) {
|
|
186
|
+
const doc = this._deps.getDoc();
|
|
187
|
+
const len = docLength(doc);
|
|
188
|
+
const sel = this._deps.getSelection();
|
|
189
|
+
// Move to document start
|
|
190
|
+
if (e.key === 'Home' || (this._deps.isMac() && e.key === 'ArrowUp')) {
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
if (e.shiftKey) {
|
|
193
|
+
// Extend selection: keep anchor, move focus to 0
|
|
194
|
+
this._deps.setSelection({
|
|
195
|
+
anchor: sel.anchor,
|
|
196
|
+
focus: { offset: 0 },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Collapse cursor to document start
|
|
201
|
+
this._deps.setSelection({
|
|
202
|
+
anchor: { offset: 0 },
|
|
203
|
+
focus: { offset: 0 },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
// Move to document end
|
|
209
|
+
if (e.key === 'End' || (this._deps.isMac() && e.key === 'ArrowDown')) {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
if (e.shiftKey) {
|
|
212
|
+
// Extend selection: keep anchor, move focus to end
|
|
213
|
+
this._deps.setSelection({
|
|
214
|
+
anchor: sel.anchor,
|
|
215
|
+
focus: { offset: len },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Collapse cursor to document end
|
|
220
|
+
this._deps.setSelection({
|
|
221
|
+
anchor: { offset: len },
|
|
222
|
+
focus: { offset: len },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=cursor-nav.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-nav.js","sourceRoot":"","sources":["../src/cursor-nav.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,KAAK;IACnB,IAAI,OAAO,SAAS,KAAK,WAAW;QAAE,OAAO,KAAK,CAAC;IACnD,OAAO,sBAAsB,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;WACjD,CAAC,SAAS,CAAC,QAAQ,KAAK,UAAU;eAC9B,OAAO,SAAS,CAAC,cAAc,KAAK,QAAQ;eAC5C,SAAS,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,QAAQ,CAAC,CAAgB;IACvC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AACzC,CAAC;AAwDD,8EAA8E;AAC9E,yBAAyB;AACzB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,gBAAgB;IACV,KAAK,CAAc;IACnB,KAAK,CAAgB;IAC9B,eAAe,GAAwC,IAAI,CAAC;IAEpE,YAAY,IAAiB,EAAE,IAAmB;QAChD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED;;;;;;;OAOG;IACH,MAAM;QACJ,IAAI,CAAC,eAAe,GAAG,CAAC,CAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAChE,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;IAC/D,CAAC;IAED;;;;;;;OAOG;IACH,MAAM;QACJ,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;YAChE,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,0EAA0E;IAC1E,qBAAqB;IACrB,0EAA0E;IAElE,UAAU,CAAC,CAAgB;QACjC,uCAAuC;QACvC,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;YAAE,OAAO;QAErC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;QAEvD,kDAAkD;QAClD,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;YACrD,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QAED,uDAAuD;QACvD,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;YACvB,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;gBAC1D,CAAC,CAAC,cAAc,EAAE,CAAC;gBACnB,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBAClB,OAAO;YACT,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;gBAC3D,CAAC,CAAC,cAAc,EAAE,CAAC;gBACnB,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBAClB,OAAO;YACT,CAAC;YACD,uCAAuC;YACvC,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;gBAC1D,CAAC,CAAC,cAAc,EAAE,CAAC;gBACnB,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBAClB,OAAO;YACT,CAAC;QACH,CAAC;QAED,qCAAqC;QACrC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;YACrD,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC;gBACtB,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE;gBACrB,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE;aACvB,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,oCAAoC;QACpC,+DAA+D;QAC/D,mEAAmE;QAEnE,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;YACrB,IAAI,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC;gBAAE,OAAO;QACzC,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACK,kBAAkB,CAAC,CAAgB;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QAEtC,yBAAyB;QACzB,IAAI,CAAC,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,KAAK,SAAS,CAAC,EAAE,CAAC;YACpE,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;gBACf,iDAAiD;gBACjD,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC;oBACtB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE;iBACrB,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,oCAAoC;gBACpC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC;oBACtB,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE;oBACrB,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE;iBACrB,CAAC,CAAC;YACL,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,uBAAuB;QACvB,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,KAAK,WAAW,CAAC,EAAE,CAAC;YACrE,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;gBACf,mDAAmD;gBACnD,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC;oBACtB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE;iBACvB,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,kCAAkC;gBAClC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC;oBACtB,MAAM,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE;oBACvB,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE;iBACvB,CAAC,CAAC;YACL,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
|