@apollohg/react-native-rich-text-editor 2.0.0 → 2.0.2
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 +8 -116
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/android/build.gradle +3 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
- package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
- package/app.plugin.js +42 -39
- package/dist/AtomHost.js +18 -11
- package/dist/EditorAddon.js +4 -2
- package/dist/EditorAddonNormalization.js +17 -9
- package/dist/EditorMentionThemeNormalization.js +6 -3
- package/dist/EditorMentionThemeValidation.js +28 -11
- package/dist/EditorStyleSheetNormalization.js +85 -38
- package/dist/EditorStyleSheetTypes.d.ts +1 -1
- package/dist/EditorTheme.d.ts +4 -0
- package/dist/EditorTheme.js +5 -3
- package/dist/EditorToolbarComponent.d.ts +1 -1
- package/dist/EditorToolbarComponent.js +1 -1
- package/dist/EditorToolbarRegistry.js +5 -5
- package/dist/EditorToolbarTypes.d.ts +12 -12
- package/dist/EditorToolbarVisuals.js +1 -1
- package/dist/ExternalTextComposition.js +89 -43
- package/dist/ImageLoadingPolicy.d.ts +1 -1
- package/dist/NativeEditorBoundaryError.d.ts +7 -7
- package/dist/NativeEditorBoundaryError.js +9 -5
- package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
- package/dist/NativeEditorCollaborationTransport.js +28 -15
- package/dist/NativeEditorCreateConfig.js +11 -6
- package/dist/NativeEditorCreateJson.js +51 -25
- package/dist/NativeEditorCreateValidation.js +11 -6
- package/dist/NativeEditorDocumentBridge.d.ts +1 -1
- package/dist/NativeEditorDocumentBridge.js +32 -18
- package/dist/NativeEditorDocumentHandle.d.ts +4 -4
- package/dist/NativeEditorDocumentHandle.js +1 -1
- package/dist/NativeEditorLocalAwareness.js +25 -13
- package/dist/NativeEditorNativeModule.d.ts +8 -1
- package/dist/NativeEditorNativeModule.js +7 -5
- package/dist/NativeEditorRenderNormalization.js +66 -25
- package/dist/NativeEditorResultNormalization.js +53 -29
- package/dist/NativeEditorTypes.d.ts +10 -10
- package/dist/NativeEditorV2Decimal.js +6 -3
- package/dist/NativeProseViewer.js +11 -3
- package/dist/ResourceLimits.d.ts +2 -2
- package/dist/ResourceLimits.js +7 -2
- package/dist/RichTextEditor.js +1 -1
- package/dist/RichTextEditorNativeTypes.d.ts +1 -1
- package/dist/RichTextEditorNativeView.d.ts +1 -3
- package/dist/RichTextEditorSerialization.js +24 -14
- package/dist/RichTextEditorTypes.d.ts +1 -1
- package/dist/YjsCollaboration.d.ts +7 -7
- package/dist/YjsCollaboration.js +42 -26
- package/dist/addons.d.ts +4 -4
- package/dist/addons.js +3 -3
- package/dist/atomInstances.js +6 -3
- package/dist/atomUpdates.js +7 -4
- package/dist/atoms.js +20 -12
- package/dist/attributeValidation.js +55 -24
- package/dist/contentExpression.js +80 -42
- package/dist/listMarkers.d.ts +1 -1
- package/dist/listMarkers.js +1 -1
- package/dist/schemaDefinition.d.ts +1 -1
- package/dist/schemaDefinition.js +23 -14
- package/dist/schemaDocument.js +1 -1
- package/dist/schemaDocumentConstruction.js +25 -14
- package/dist/schemaNormalization.js +51 -29
- package/dist/schemaPresets.d.ts +1 -1
- package/dist/schemaPresets.js +9 -4
- package/dist/schemaResolution.js +46 -25
- package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
- package/dist/useEditorToolbarInteractions.d.ts +2 -2
- package/dist/useEditorToolbarInteractions.js +45 -23
- package/dist/useEditorToolbarItems.d.ts +1 -1
- package/dist/useEditorToolbarItems.js +4 -3
- package/dist/useEditorToolbarPresentation.d.ts +3 -3
- package/dist/useEditorToolbarPresentation.js +4 -4
- package/dist/useEditorToolbarState.d.ts +1 -1
- package/dist/useEditorToolbarState.js +1 -1
- package/dist/useFocusPreservingFrames.js +11 -8
- package/dist/useNativeEditor.d.ts +1 -1
- package/dist/useNativeEditor.js +21 -11
- package/dist/useRichTextEditorCommands.d.ts +2 -2
- package/dist/useRichTextEditorCommands.js +59 -29
- package/dist/useRichTextEditorEvents.d.ts +3 -3
- package/dist/useRichTextEditorEvents.js +75 -32
- package/dist/useRichTextEditorMentions.d.ts +4 -4
- package/dist/useRichTextEditorMentions.js +48 -29
- package/dist/useRichTextEditorPresentation.d.ts +4 -4
- package/dist/useRichTextEditorPresentation.js +25 -23
- package/dist/useRichTextEditorState.d.ts +3 -3
- package/dist/useRichTextEditorState.js +20 -15
- package/dist/useRichTextEditorUpdates.d.ts +1 -1
- package/dist/useRichTextEditorUpdates.js +99 -41
- package/dist/useViewerAtoms.js +63 -36
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorInteractionOverlays.swift +114 -122
- package/ios/EditorLayoutManager.swift +13 -3
- package/ios/EditorStyleSheet.swift +12 -1
- package/ios/EditorTextView+Images.swift +32 -10
- package/ios/EditorTextView.swift +1 -1
- package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
- package/ios/NativeToolbar+ButtonStyle.swift +17 -5
- package/ios/NativeToolbar+Diagnostics.swift +15 -0
- package/ios/RenderBridge+Atoms.swift +0 -1
- package/ios/RenderBridge+Attachments.swift +0 -29
- package/ios/RichTextEditorView.swift +4 -12
- package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
- package/package.json +11 -3
- package/react-native.config.js +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/src/AtomHost.tsx +58 -28
- package/src/EditorAddon.ts +18 -8
- package/src/EditorAddonNormalization.ts +40 -11
- package/src/EditorMentionThemeNormalization.ts +15 -4
- package/src/EditorMentionThemeValidation.ts +51 -14
- package/src/EditorStyleSheet.ts +1 -0
- package/src/EditorStyleSheetNormalization.ts +195 -64
- package/src/EditorStyleSheetTypes.ts +1 -1
- package/src/EditorTheme.ts +13 -5
- package/src/EditorToolbarComponent.tsx +2 -1
- package/src/EditorToolbarRegistry.tsx +23 -13
- package/src/EditorToolbarTypes.tsx +12 -12
- package/src/EditorToolbarVisuals.tsx +6 -3
- package/src/EditorUpdateRevision.ts +1 -0
- package/src/ExternalTextComposition.ts +259 -49
- package/src/ImageLoadingPolicy.ts +3 -1
- package/src/NativeEditorBoundaryError.ts +38 -11
- package/src/NativeEditorCollaborationTransport.ts +108 -27
- package/src/NativeEditorCreateConfig.ts +92 -19
- package/src/NativeEditorCreateJson.ts +163 -26
- package/src/NativeEditorCreateValidation.ts +33 -12
- package/src/NativeEditorDocumentBridge.ts +111 -19
- package/src/NativeEditorDocumentHandle.ts +9 -4
- package/src/NativeEditorLocalAwareness.ts +70 -15
- package/src/NativeEditorNativeModule.ts +13 -9
- package/src/NativeEditorRenderNormalization.ts +128 -29
- package/src/NativeEditorResultNormalization.ts +143 -32
- package/src/NativeEditorTypes.ts +12 -11
- package/src/NativeEditorV2Decimal.ts +12 -3
- package/src/NativeProseViewer.tsx +46 -16
- package/src/ResourceLimits.ts +15 -8
- package/src/RichTextEditor.tsx +3 -1
- package/src/RichTextEditorNativeTypes.tsx +1 -1
- package/src/RichTextEditorNativeView.tsx +2 -8
- package/src/RichTextEditorSerialization.tsx +70 -24
- package/src/RichTextEditorTypes.tsx +1 -1
- package/src/YjsCollaboration.ts +143 -47
- package/src/addons.ts +12 -8
- package/src/atomInstances.ts +25 -5
- package/src/atomUpdates.ts +23 -6
- package/src/atoms.ts +85 -18
- package/src/attributeValidation.ts +97 -33
- package/src/contentExpression.ts +218 -63
- package/src/listMarkers.ts +1 -1
- package/src/schemaDefinition.ts +82 -20
- package/src/schemaDocument.ts +15 -1
- package/src/schemaDocumentConstruction.ts +80 -16
- package/src/schemaNormalization.ts +155 -41
- package/src/schemaPresets.ts +37 -31
- package/src/schemaResolution.ts +126 -27
- package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
- package/src/useEditorToolbarInteractions.tsx +85 -36
- package/src/useEditorToolbarItems.tsx +50 -22
- package/src/useEditorToolbarPresentation.tsx +81 -58
- package/src/useEditorToolbarState.tsx +8 -6
- package/src/useFocusPreservingFrames.ts +43 -15
- package/src/useNativeEditor.ts +111 -51
- package/src/useRichTextEditorCommands.tsx +127 -54
- package/src/useRichTextEditorEvents.tsx +144 -42
- package/src/useRichTextEditorMentions.tsx +118 -37
- package/src/useRichTextEditorPresentation.tsx +128 -114
- package/src/useRichTextEditorState.tsx +76 -40
- package/src/useRichTextEditorUpdates.tsx +207 -56
- package/src/useViewerAtoms.tsx +162 -66
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
NativeEditorErrorBase,
|
|
2
|
+
type NativeEditorErrorBase,
|
|
3
3
|
NativeEditorNonRetryableError,
|
|
4
4
|
nativeEditorV2ErrorToException,
|
|
5
5
|
normalizeNativeEditorV2Error,
|
|
@@ -69,12 +69,18 @@ import {
|
|
|
69
69
|
*/
|
|
70
70
|
export class NativeEditorDocumentBridge {
|
|
71
71
|
private readonly _editorId: string;
|
|
72
|
+
|
|
72
73
|
private _destroyed = false;
|
|
74
|
+
|
|
73
75
|
/** @internal Reset intent waiting for the interactive view handoff. */
|
|
74
76
|
_pendingViewReset: { json: string } | null = null;
|
|
77
|
+
|
|
75
78
|
private _nextRequestId = 0n;
|
|
79
|
+
|
|
76
80
|
private readonly _errorListeners = new Set<(error: NativeEditorErrorBase) => void>();
|
|
81
|
+
|
|
77
82
|
private _collaborationProtocolAdapter: NativeCollaborationProtocolAdapter | null = null;
|
|
83
|
+
|
|
78
84
|
private _collaborationProtocolAdapterSubscription: { remove(): void } | null = null;
|
|
79
85
|
|
|
80
86
|
/** @internal Created by createNativeEditorDocumentHandle. */
|
|
@@ -91,15 +97,21 @@ export class NativeEditorDocumentBridge {
|
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
private assertAlive(): void {
|
|
94
|
-
if (this._destroyed)
|
|
100
|
+
if (this._destroyed) {
|
|
101
|
+
throw destroyedHandleError();
|
|
102
|
+
}
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
private callV2<T>(invoke: () => unknown, normalizeValue: (value: unknown) => T | null): T {
|
|
98
106
|
this.assertAlive();
|
|
99
107
|
const raw = invoke();
|
|
108
|
+
|
|
100
109
|
// A re-entrant destroy racing the native call makes any result
|
|
101
110
|
// arriving now a result for a destroyed handle: non-retryable.
|
|
102
|
-
if (this._destroyed)
|
|
111
|
+
if (this._destroyed) {
|
|
112
|
+
throw destroyedHandleError();
|
|
113
|
+
}
|
|
114
|
+
|
|
103
115
|
return unwrapNativeEditorV2Result(raw, normalizeValue);
|
|
104
116
|
}
|
|
105
117
|
|
|
@@ -107,7 +119,9 @@ export class NativeEditorDocumentBridge {
|
|
|
107
119
|
if (this._nextRequestId >= 18_446_744_073_709_551_615n) {
|
|
108
120
|
throw invalidV2RequestError('NativeEditorBridge: v2 request id exhausted');
|
|
109
121
|
}
|
|
122
|
+
|
|
110
123
|
this._nextRequestId += 1n;
|
|
124
|
+
|
|
111
125
|
return this._nextRequestId.toString();
|
|
112
126
|
}
|
|
113
127
|
|
|
@@ -122,19 +136,28 @@ export class NativeEditorDocumentBridge {
|
|
|
122
136
|
`"version":${V2_ENVELOPE_VERSION}`,
|
|
123
137
|
`"requestId":"${this.nextRequestId()}"`,
|
|
124
138
|
];
|
|
139
|
+
|
|
125
140
|
if (baseDocumentRevision !== undefined) {
|
|
126
141
|
const digits = requireV2DecimalId(baseDocumentRevision, 'baseDocumentRevision');
|
|
127
142
|
parts.push(`"baseDocumentRevision":"${digits}"`);
|
|
128
143
|
}
|
|
144
|
+
|
|
129
145
|
const payloadJson = JSON.stringify(payload);
|
|
130
146
|
const inner = payloadJson.slice(1, payloadJson.length - 1);
|
|
131
|
-
|
|
147
|
+
|
|
148
|
+
if (inner.length > 0) {
|
|
149
|
+
parts.push(inner);
|
|
150
|
+
}
|
|
151
|
+
|
|
132
152
|
return `{${parts.join(',')}}`;
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
/** Destroy the session. Repeated destroy is safe. */
|
|
136
156
|
destroy(): void {
|
|
137
|
-
if (this._destroyed)
|
|
157
|
+
if (this._destroyed) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
138
161
|
try {
|
|
139
162
|
unwrapNativeEditorV2Result(
|
|
140
163
|
invokeNativeEditorV2('editorV2Destroy', this._editorId),
|
|
@@ -153,6 +176,7 @@ export class NativeEditorDocumentBridge {
|
|
|
153
176
|
throw error;
|
|
154
177
|
}
|
|
155
178
|
}
|
|
179
|
+
|
|
156
180
|
this._destroyed = true;
|
|
157
181
|
this._collaborationProtocolAdapter = null;
|
|
158
182
|
this._collaborationProtocolAdapterSubscription?.remove();
|
|
@@ -163,6 +187,7 @@ export class NativeEditorDocumentBridge {
|
|
|
163
187
|
/** Subscribe to autonomous native failures; returns the unsubscribe. */
|
|
164
188
|
addErrorListener(listener: (error: NativeEditorErrorBase) => void): () => void {
|
|
165
189
|
this._errorListeners.add(listener);
|
|
190
|
+
|
|
166
191
|
return () => {
|
|
167
192
|
this._errorListeners.delete(listener);
|
|
168
193
|
};
|
|
@@ -175,13 +200,18 @@ export class NativeEditorDocumentBridge {
|
|
|
175
200
|
* contract violation so the view stays usable.
|
|
176
201
|
*/
|
|
177
202
|
_emitAutonomousError(raw: unknown): void {
|
|
178
|
-
if (this._destroyed)
|
|
203
|
+
if (this._destroyed) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
179
207
|
const candidate = isPlainRecord(raw) && 'error' in raw ? raw : { error: raw };
|
|
180
208
|
const normalized = normalizeNativeEditorV2Error(candidate);
|
|
209
|
+
|
|
181
210
|
const exception =
|
|
182
211
|
normalized == null
|
|
183
212
|
? invalidV2ResultError()
|
|
184
213
|
: nativeEditorV2ErrorToException(normalized);
|
|
214
|
+
|
|
185
215
|
for (const listener of this._errorListeners) {
|
|
186
216
|
listener(exception);
|
|
187
217
|
}
|
|
@@ -228,13 +258,21 @@ export class NativeEditorDocumentBridge {
|
|
|
228
258
|
this.assertAlive();
|
|
229
259
|
const mirrorAnchor = mirrorScalarSelection?.anchor ?? null;
|
|
230
260
|
const mirrorHead = mirrorScalarSelection?.head ?? null;
|
|
261
|
+
|
|
231
262
|
if ((mirrorAnchor == null) !== (mirrorHead == null)) {
|
|
232
263
|
throw invalidV2RequestError(
|
|
233
264
|
'NativeEditorBridge: render update mirror requires both scalar anchor and head'
|
|
234
265
|
);
|
|
235
266
|
}
|
|
236
|
-
|
|
237
|
-
if (
|
|
267
|
+
|
|
268
|
+
if (mirrorAnchor != null) {
|
|
269
|
+
requireNativeEditorV2U32(mirrorAnchor, 'mirrorScalarAnchor');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (mirrorHead != null) {
|
|
273
|
+
requireNativeEditorV2U32(mirrorHead, 'mirrorScalarHead');
|
|
274
|
+
}
|
|
275
|
+
|
|
238
276
|
return this.callV2(
|
|
239
277
|
() =>
|
|
240
278
|
invokeNativeEditorV2(
|
|
@@ -250,14 +288,23 @@ export class NativeEditorDocumentBridge {
|
|
|
250
288
|
replaceDocument(request: NativeEditorReplaceDocumentRequest): NativeEditorCommitInfo {
|
|
251
289
|
this.assertAlive();
|
|
252
290
|
const payload: Record<string, unknown> = {};
|
|
253
|
-
|
|
254
|
-
if (request.
|
|
291
|
+
|
|
292
|
+
if (request.setJson !== undefined) {
|
|
293
|
+
payload.setJson = request.setJson;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (request.setHtml !== undefined) {
|
|
297
|
+
payload.setHtml = request.setHtml;
|
|
298
|
+
}
|
|
299
|
+
|
|
255
300
|
payload.history = request.history;
|
|
256
301
|
const requestJson = this.buildEnvelopeJson(payload);
|
|
302
|
+
|
|
257
303
|
const result = this.callV2(
|
|
258
304
|
() => invokeNativeEditorV2('editorV2ReplaceDocument', this._editorId, requestJson),
|
|
259
305
|
normalizeNativeEditorV2CommitValue
|
|
260
306
|
);
|
|
307
|
+
|
|
261
308
|
if (request.history === 'resetAndClear' && 'documentRevision' in result) {
|
|
262
309
|
this._pendingViewReset = {
|
|
263
310
|
json: JSON.stringify({ ...payload, documentRevision: result.documentRevision }),
|
|
@@ -265,15 +312,18 @@ export class NativeEditorDocumentBridge {
|
|
|
265
312
|
} else {
|
|
266
313
|
this._pendingViewReset = null;
|
|
267
314
|
}
|
|
315
|
+
|
|
268
316
|
return result;
|
|
269
317
|
}
|
|
270
318
|
|
|
271
319
|
applyInput(request: NativeEditorInputRequest): NativeEditorMutationOutcome {
|
|
272
320
|
this.assertAlive();
|
|
321
|
+
|
|
273
322
|
const requestJson = this.buildEnvelopeJson(
|
|
274
323
|
{ text: request.text },
|
|
275
324
|
request.baseDocumentRevision
|
|
276
325
|
);
|
|
326
|
+
|
|
277
327
|
return this.callV2(
|
|
278
328
|
() => invokeNativeEditorV2('editorV2ApplyInput', this._editorId, requestJson),
|
|
279
329
|
normalizeNativeEditorV2MutationOutcomeValue
|
|
@@ -282,10 +332,12 @@ export class NativeEditorDocumentBridge {
|
|
|
282
332
|
|
|
283
333
|
applyCommand(request: NativeEditorCommandRequest): NativeEditorMutationOutcome {
|
|
284
334
|
this.assertAlive();
|
|
335
|
+
|
|
285
336
|
const requestJson = this.buildEnvelopeJson(
|
|
286
337
|
{ command: request.command },
|
|
287
338
|
request.baseDocumentRevision
|
|
288
339
|
);
|
|
340
|
+
|
|
289
341
|
return this.callV2(
|
|
290
342
|
() => invokeNativeEditorV2('editorV2ApplyCommand', this._editorId, requestJson),
|
|
291
343
|
normalizeNativeEditorV2MutationOutcomeValue
|
|
@@ -295,14 +347,23 @@ export class NativeEditorDocumentBridge {
|
|
|
295
347
|
applyLocalApi(request: NativeEditorLocalApiRequest): NativeEditorMutationOutcome {
|
|
296
348
|
this.assertAlive();
|
|
297
349
|
const payload: Record<string, unknown> = {};
|
|
298
|
-
|
|
299
|
-
if (request.
|
|
350
|
+
|
|
351
|
+
if (request.setJson !== undefined) {
|
|
352
|
+
payload.setJson = request.setJson;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (request.setHtml !== undefined) {
|
|
356
|
+
payload.setHtml = request.setHtml;
|
|
357
|
+
}
|
|
358
|
+
|
|
300
359
|
payload.history = request.history;
|
|
301
360
|
const requestJson = this.buildEnvelopeJson(payload, request.baseDocumentRevision);
|
|
361
|
+
|
|
302
362
|
const result = this.callV2(
|
|
303
363
|
() => invokeNativeEditorV2('editorV2ApplyLocalApi', this._editorId, requestJson),
|
|
304
364
|
normalizeNativeEditorV2MutationOutcomeValue
|
|
305
365
|
);
|
|
366
|
+
|
|
306
367
|
if (request.history === 'resetAndClear' && 'documentRevision' in result) {
|
|
307
368
|
this._pendingViewReset = {
|
|
308
369
|
json: JSON.stringify({ ...payload, documentRevision: result.documentRevision }),
|
|
@@ -310,15 +371,18 @@ export class NativeEditorDocumentBridge {
|
|
|
310
371
|
} else {
|
|
311
372
|
this._pendingViewReset = null;
|
|
312
373
|
}
|
|
374
|
+
|
|
313
375
|
return result;
|
|
314
376
|
}
|
|
315
377
|
|
|
316
378
|
setSelection(request: NativeEditorSelectionRequest): NativeEditorMutationOutcome {
|
|
317
379
|
this.assertAlive();
|
|
380
|
+
|
|
318
381
|
const requestJson = this.buildEnvelopeJson(
|
|
319
382
|
{ selection: request.selection },
|
|
320
383
|
request.baseDocumentRevision
|
|
321
384
|
);
|
|
385
|
+
|
|
322
386
|
return this.callV2(
|
|
323
387
|
() => invokeNativeEditorV2('editorV2SetSelection', this._editorId, requestJson),
|
|
324
388
|
normalizeNativeEditorV2MutationOutcomeValue
|
|
@@ -328,6 +392,7 @@ export class NativeEditorDocumentBridge {
|
|
|
328
392
|
undo(): boolean {
|
|
329
393
|
this.assertAlive();
|
|
330
394
|
const requestJson = this.buildEnvelopeJson({});
|
|
395
|
+
|
|
331
396
|
return this.callV2(
|
|
332
397
|
() => invokeNativeEditorV2('editorV2Undo', this._editorId, requestJson),
|
|
333
398
|
normalizeNativeEditorV2ChangedValue
|
|
@@ -337,6 +402,7 @@ export class NativeEditorDocumentBridge {
|
|
|
337
402
|
redo(): boolean {
|
|
338
403
|
this.assertAlive();
|
|
339
404
|
const requestJson = this.buildEnvelopeJson({});
|
|
405
|
+
|
|
340
406
|
return this.callV2(
|
|
341
407
|
() => invokeNativeEditorV2('editorV2Redo', this._editorId, requestJson),
|
|
342
408
|
normalizeNativeEditorV2ChangedValue
|
|
@@ -356,6 +422,7 @@ export class NativeEditorDocumentBridge {
|
|
|
356
422
|
): NativeEditorCommitInfo {
|
|
357
423
|
this.assertAlive();
|
|
358
424
|
const bytes = requireV2Bytes(encodedState, 'snapshot encodedState');
|
|
425
|
+
|
|
359
426
|
return this.callV2(
|
|
360
427
|
() =>
|
|
361
428
|
invokeNativeEditorV2(
|
|
@@ -373,13 +440,16 @@ export class NativeEditorDocumentBridge {
|
|
|
373
440
|
const wireConfig = config === null ? null : collaborationTransportWireConfig(config);
|
|
374
441
|
const previousAdapter = this._collaborationProtocolAdapter;
|
|
375
442
|
const nextAdapter = config?.protocolAdapter ?? null;
|
|
443
|
+
|
|
376
444
|
if (nextAdapter !== null && this._collaborationProtocolAdapterSubscription === null) {
|
|
377
445
|
this._collaborationProtocolAdapterSubscription = getNativeModule().addListener(
|
|
378
446
|
'onCollaborationTransportEvent',
|
|
379
|
-
|
|
447
|
+
rawEvent => this.handleCollaborationProtocolAdapterEvent(rawEvent)
|
|
380
448
|
);
|
|
381
449
|
}
|
|
450
|
+
|
|
382
451
|
this._collaborationProtocolAdapter = nextAdapter;
|
|
452
|
+
|
|
383
453
|
try {
|
|
384
454
|
this.callV2(
|
|
385
455
|
() =>
|
|
@@ -392,6 +462,7 @@ export class NativeEditorDocumentBridge {
|
|
|
392
462
|
);
|
|
393
463
|
} catch (error) {
|
|
394
464
|
this._collaborationProtocolAdapter = previousAdapter;
|
|
465
|
+
|
|
395
466
|
if (
|
|
396
467
|
previousAdapter === null &&
|
|
397
468
|
this._collaborationProtocolAdapterSubscription !== null
|
|
@@ -399,8 +470,10 @@ export class NativeEditorDocumentBridge {
|
|
|
399
470
|
this._collaborationProtocolAdapterSubscription.remove();
|
|
400
471
|
this._collaborationProtocolAdapterSubscription = null;
|
|
401
472
|
}
|
|
473
|
+
|
|
402
474
|
throw error;
|
|
403
475
|
}
|
|
476
|
+
|
|
404
477
|
if (nextAdapter === null && this._collaborationProtocolAdapterSubscription !== null) {
|
|
405
478
|
this._collaborationProtocolAdapterSubscription.remove();
|
|
406
479
|
this._collaborationProtocolAdapterSubscription = null;
|
|
@@ -408,27 +481,38 @@ export class NativeEditorDocumentBridge {
|
|
|
408
481
|
}
|
|
409
482
|
|
|
410
483
|
private handleCollaborationProtocolAdapterEvent(rawEvent: unknown): void {
|
|
411
|
-
if (this._destroyed || this._collaborationProtocolAdapter === null)
|
|
484
|
+
if (this._destroyed || this._collaborationProtocolAdapter === null) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
412
488
|
const event = normalizeNativeCollaborationTransportEvent(rawEvent);
|
|
489
|
+
|
|
413
490
|
if (event?.editorId !== this._editorId || event.kind !== 'protocolAdapter') {
|
|
414
491
|
return;
|
|
415
492
|
}
|
|
493
|
+
|
|
416
494
|
const adapter = this._collaborationProtocolAdapter;
|
|
495
|
+
|
|
417
496
|
const context: NativeCollaborationProtocolAdapterContext = {
|
|
418
497
|
attemptId: event.attemptId,
|
|
419
498
|
generation: event.generation,
|
|
420
499
|
negotiatedProtocol: event.negotiatedProtocol,
|
|
421
500
|
};
|
|
501
|
+
|
|
422
502
|
const callback =
|
|
423
503
|
event.phase === 'open'
|
|
424
504
|
? () => adapter.onOpen(context)
|
|
425
505
|
: () => adapter.onMessage(context, event.frame!);
|
|
506
|
+
|
|
426
507
|
void Promise.resolve()
|
|
427
508
|
.then(callback)
|
|
428
|
-
.then(
|
|
509
|
+
.then(result => serializeCollaborationProtocolAdapterResult(result))
|
|
429
510
|
.catch(() => '{"action":"reject"}')
|
|
430
|
-
.then(
|
|
431
|
-
if (this._destroyed)
|
|
511
|
+
.then(responseJson => {
|
|
512
|
+
if (this._destroyed) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
432
516
|
try {
|
|
433
517
|
this.callV2(
|
|
434
518
|
() =>
|
|
@@ -451,10 +535,12 @@ export class NativeEditorDocumentBridge {
|
|
|
451
535
|
|
|
452
536
|
setLocalAwareness(intent: NativeEditorLocalAwarenessIntent | null): void {
|
|
453
537
|
this.assertAlive();
|
|
538
|
+
|
|
454
539
|
const awarenessJson =
|
|
455
540
|
intent === null
|
|
456
541
|
? 'null'
|
|
457
542
|
: serializeLocalAwarenessIntent(validateLocalAwarenessIntent(intent));
|
|
543
|
+
|
|
458
544
|
this.callV2(
|
|
459
545
|
() =>
|
|
460
546
|
invokeNativeEditorV2(
|
|
@@ -470,16 +556,22 @@ export class NativeEditorDocumentBridge {
|
|
|
470
556
|
listener: (event: NativeCollaborationTransportEvent) => void
|
|
471
557
|
): () => void {
|
|
472
558
|
this.assertAlive();
|
|
559
|
+
|
|
473
560
|
const subscription = getNativeModule().addListener(
|
|
474
561
|
'onCollaborationTransportEvent',
|
|
475
|
-
|
|
476
|
-
if (this._destroyed)
|
|
562
|
+
rawEvent => {
|
|
563
|
+
if (this._destroyed) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
477
567
|
const event = normalizeNativeCollaborationTransportEvent(rawEvent);
|
|
568
|
+
|
|
478
569
|
if (event?.editorId === this._editorId) {
|
|
479
570
|
listener(event);
|
|
480
571
|
}
|
|
481
572
|
}
|
|
482
573
|
);
|
|
574
|
+
|
|
483
575
|
return () => subscription.remove();
|
|
484
576
|
}
|
|
485
577
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ResolvedDocumentSchema } from './schemas';
|
|
2
|
-
import { NativeEditorErrorBase } from './NativeEditorBoundaryError';
|
|
2
|
+
import { type NativeEditorErrorBase } from './NativeEditorBoundaryError';
|
|
3
3
|
import { NativeEditorDocumentBridge } from './NativeEditorDocumentBridge';
|
|
4
4
|
import {
|
|
5
5
|
type NativeCollaborationTransportConfig,
|
|
@@ -34,8 +34,8 @@ export const NATIVE_EDITOR_DOCUMENT_HANDLE_DESCRIPTORS = new WeakMap<
|
|
|
34
34
|
* One native document session, shared by everything that touches the
|
|
35
35
|
* document: the editor view, the headless `useNativeEditorDocument` binding,
|
|
36
36
|
* and the collaboration controller. Obtain one from
|
|
37
|
-
* {@link createNativeEditorDocumentHandle}
|
|
38
|
-
* directly
|
|
37
|
+
* {@link createNativeEditorDocumentHandle} : it cannot be constructed
|
|
38
|
+
* directly : and `destroy()` it when its owner unmounts.
|
|
39
39
|
*/
|
|
40
40
|
export interface NativeEditorDocumentHandle {
|
|
41
41
|
readonly [NATIVE_EDITOR_DOCUMENT_HANDLE_BRAND]: true;
|
|
@@ -72,6 +72,7 @@ export class NativeEditorDocumentHandleImpl implements NativeEditorDocumentHandl
|
|
|
72
72
|
'NativeEditorBridge: NativeEditorDocumentHandle cannot be constructed directly'
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
|
+
|
|
75
76
|
AUTHENTIC_NATIVE_EDITOR_DOCUMENT_HANDLES.add(this);
|
|
76
77
|
NATIVE_EDITOR_DOCUMENT_HANDLE_DESCRIPTORS.set(this, documentDescriptor);
|
|
77
78
|
}
|
|
@@ -109,11 +110,13 @@ export function _getNativeEditorDocumentHandleDescriptor(
|
|
|
109
110
|
): ResolvedDocumentSchema {
|
|
110
111
|
_assertNativeEditorDocumentHandle(handle);
|
|
111
112
|
const documentDescriptor = NATIVE_EDITOR_DOCUMENT_HANDLE_DESCRIPTORS.get(handle);
|
|
113
|
+
|
|
112
114
|
if (documentDescriptor === undefined) {
|
|
113
115
|
throw invalidV2RequestError(
|
|
114
116
|
'NativeEditorBridge: authentic NativeEditorDocumentHandle has no document descriptor'
|
|
115
117
|
);
|
|
116
118
|
}
|
|
119
|
+
|
|
117
120
|
return documentDescriptor;
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -134,7 +137,7 @@ export function _assertNativeEditorDocumentHandle(
|
|
|
134
137
|
|
|
135
138
|
/**
|
|
136
139
|
* Create the native document session every other API binds to. Create it once
|
|
137
|
-
* per document
|
|
140
|
+
* per document : `useMemo`, not on each render : and destroy it when its
|
|
138
141
|
* owner unmounts.
|
|
139
142
|
*
|
|
140
143
|
* @throws NativeEditorErrorBase when the config is rejected: a malformed
|
|
@@ -155,10 +158,12 @@ export function createNativeEditorDocumentHandle(
|
|
|
155
158
|
config: NativeEditorCreateConfig
|
|
156
159
|
): NativeEditorDocumentHandle {
|
|
157
160
|
const { configJson, snapshotState, documentDescriptor } = buildV2CreateRequest(config);
|
|
161
|
+
|
|
158
162
|
const value = unwrapNativeEditorV2Result(
|
|
159
163
|
invokeNativeEditorV2('editorV2Create', configJson, snapshotState),
|
|
160
164
|
normalizeNativeEditorV2CreateValue
|
|
161
165
|
);
|
|
166
|
+
|
|
162
167
|
return new NativeEditorDocumentHandleImpl(
|
|
163
168
|
NATIVE_EDITOR_DOCUMENT_HANDLE_TOKEN,
|
|
164
169
|
value.editorId,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from './NativeEditorTypes';
|
|
7
7
|
import { normalizeV2JsonValue, serializeV2CreateEnvelope } from './NativeEditorCreateJson';
|
|
8
8
|
|
|
9
|
-
export const LOCAL_AWARENESS_INTENT_KEYS = new Set(['state', 'focused', 'selection']);
|
|
9
|
+
export const LOCAL_AWARENESS_INTENT_KEYS = new Set([ 'state', 'focused', 'selection' ]);
|
|
10
10
|
|
|
11
11
|
export const LOCAL_AWARENESS_SELECTION_VALUES = new WeakMap<
|
|
12
12
|
object,
|
|
@@ -28,26 +28,37 @@ export function createNativeEditorLocalAwarenessSelection(
|
|
|
28
28
|
): NativeEditorLocalAwarenessSelection {
|
|
29
29
|
const acceptedAnchor = nativeEditorV2U32(anchor);
|
|
30
30
|
const acceptedHead = nativeEditorV2U32(head);
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
if (acceptedAnchor == null || acceptedHead == null) {
|
|
33
|
+
invalidLocalAwarenessIntent();
|
|
34
|
+
}
|
|
32
35
|
|
|
33
36
|
const selection: NativeEditorLocalAwarenessSelection =
|
|
34
37
|
new NativeEditorLocalAwarenessSelectionValue(acceptedAnchor, acceptedHead);
|
|
38
|
+
|
|
35
39
|
Object.freeze(selection);
|
|
36
40
|
LOCAL_AWARENESS_SELECTION_VALUES.set(selection, selection);
|
|
41
|
+
|
|
37
42
|
return selection;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export function isLocalAwarenessRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
-
if (value == null || typeof value !== 'object' || Array.isArray(value))
|
|
46
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
const prototype = Object.getPrototypeOf(value);
|
|
51
|
+
|
|
43
52
|
return prototype === Object.prototype || prototype === null;
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
export function localAwarenessOwnDataValue(record: Record<string, unknown>, key: string): unknown {
|
|
47
56
|
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
57
|
+
|
|
48
58
|
if (descriptor === undefined || !('value' in descriptor) || descriptor.enumerable !== true) {
|
|
49
59
|
invalidLocalAwarenessIntent();
|
|
50
60
|
}
|
|
61
|
+
|
|
51
62
|
return descriptor.value;
|
|
52
63
|
}
|
|
53
64
|
|
|
@@ -55,35 +66,58 @@ export function validateLocalAwarenessSelection(selection: unknown): {
|
|
|
55
66
|
anchor: number;
|
|
56
67
|
head: number;
|
|
57
68
|
} {
|
|
58
|
-
if (selection == null || typeof selection !== 'object')
|
|
69
|
+
if (selection == null || typeof selection !== 'object') {
|
|
70
|
+
invalidLocalAwarenessIntent();
|
|
71
|
+
}
|
|
59
72
|
|
|
60
73
|
// Do not inspect caller-held values before provenance succeeds: a Proxy
|
|
61
74
|
// can imitate every structural and own-data check, but it cannot inherit
|
|
62
75
|
// this module's WeakMap identity.
|
|
63
76
|
const factoryValue = LOCAL_AWARENESS_SELECTION_VALUES.get(selection);
|
|
64
|
-
|
|
77
|
+
|
|
78
|
+
if (factoryValue === undefined) {
|
|
79
|
+
invalidLocalAwarenessIntent();
|
|
80
|
+
}
|
|
65
81
|
|
|
66
82
|
const anchor = nativeEditorV2U32(factoryValue.anchor);
|
|
67
83
|
const head = nativeEditorV2U32(factoryValue.head);
|
|
84
|
+
|
|
68
85
|
if (anchor == null || head == null) {
|
|
69
86
|
invalidLocalAwarenessIntent();
|
|
70
87
|
}
|
|
88
|
+
|
|
71
89
|
return { anchor, head };
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
/** Reject caller-owned sticky cursor data before a native call can occur. */
|
|
75
93
|
export function rejectReservedAwarenessCursor(value: unknown): void {
|
|
76
|
-
const pending: unknown[] = [value];
|
|
94
|
+
const pending: unknown[] = [ value ];
|
|
77
95
|
const seen = new WeakSet<object>();
|
|
96
|
+
|
|
78
97
|
while (pending.length > 0) {
|
|
79
98
|
const current = pending.pop();
|
|
80
|
-
|
|
81
|
-
if (
|
|
99
|
+
|
|
100
|
+
if (current == null || typeof current !== 'object') {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (seen.has(current)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
82
108
|
seen.add(current);
|
|
109
|
+
|
|
83
110
|
for (const key of Reflect.ownKeys(current)) {
|
|
84
|
-
if (key === 'cursor')
|
|
111
|
+
if (key === 'cursor') {
|
|
112
|
+
invalidLocalAwarenessIntent('reserved cursor key is not allowed');
|
|
113
|
+
}
|
|
114
|
+
|
|
85
115
|
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
86
|
-
|
|
116
|
+
|
|
117
|
+
if (descriptor == null || !('value' in descriptor)) {
|
|
118
|
+
invalidLocalAwarenessIntent();
|
|
119
|
+
}
|
|
120
|
+
|
|
87
121
|
pending.push(descriptor.value);
|
|
88
122
|
}
|
|
89
123
|
}
|
|
@@ -95,13 +129,19 @@ export function normalizeLocalAwarenessState(value: unknown): Record<string, unk
|
|
|
95
129
|
seen: new WeakSet<object>(),
|
|
96
130
|
work: 0,
|
|
97
131
|
});
|
|
132
|
+
|
|
98
133
|
if (!isLocalAwarenessRecord(normalized) || Object.getPrototypeOf(normalized) !== null) {
|
|
99
134
|
invalidLocalAwarenessIntent();
|
|
100
135
|
}
|
|
136
|
+
|
|
101
137
|
rejectReservedAwarenessCursor(normalized);
|
|
138
|
+
|
|
102
139
|
return normalized;
|
|
103
140
|
} catch (error) {
|
|
104
|
-
if (error instanceof NativeEditorEngineBoundaryError)
|
|
141
|
+
if (error instanceof NativeEditorEngineBoundaryError) {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
|
|
105
145
|
invalidLocalAwarenessIntent();
|
|
106
146
|
}
|
|
107
147
|
}
|
|
@@ -120,27 +160,40 @@ export function validateLocalAwarenessIntent(
|
|
|
120
160
|
if (
|
|
121
161
|
!isLocalAwarenessRecord(intent) ||
|
|
122
162
|
Reflect.ownKeys(intent).some(
|
|
123
|
-
|
|
163
|
+
key => typeof key !== 'string' || !LOCAL_AWARENESS_INTENT_KEYS.has(key)
|
|
124
164
|
) ||
|
|
125
165
|
!Object.prototype.hasOwnProperty.call(intent, 'state') ||
|
|
126
166
|
!Object.prototype.hasOwnProperty.call(intent, 'focused')
|
|
127
167
|
) {
|
|
128
168
|
invalidLocalAwarenessIntent();
|
|
129
169
|
}
|
|
170
|
+
|
|
130
171
|
const state = normalizeLocalAwarenessState(localAwarenessOwnDataValue(intent, 'state'));
|
|
131
172
|
const focused = localAwarenessOwnDataValue(intent, 'focused');
|
|
132
|
-
|
|
173
|
+
|
|
174
|
+
if (typeof focused !== 'boolean') {
|
|
175
|
+
invalidLocalAwarenessIntent();
|
|
176
|
+
}
|
|
133
177
|
|
|
134
178
|
if (!Object.prototype.hasOwnProperty.call(intent, 'selection')) {
|
|
135
179
|
// Absent: retain whatever cursor Rust already holds.
|
|
136
180
|
return { state, focused };
|
|
137
181
|
}
|
|
182
|
+
|
|
138
183
|
const rawSelection = localAwarenessOwnDataValue(intent, 'selection');
|
|
139
|
-
|
|
184
|
+
|
|
185
|
+
if (rawSelection === null) {
|
|
186
|
+
return { state, focused, selection: null };
|
|
187
|
+
}
|
|
188
|
+
|
|
140
189
|
const selection = validateLocalAwarenessSelection(rawSelection);
|
|
190
|
+
|
|
141
191
|
return { state, focused, selection: { type: 'text', ...selection } };
|
|
142
192
|
} catch (error) {
|
|
143
|
-
if (error instanceof NativeEditorEngineBoundaryError)
|
|
193
|
+
if (error instanceof NativeEditorEngineBoundaryError) {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
|
|
144
197
|
invalidLocalAwarenessIntent();
|
|
145
198
|
}
|
|
146
199
|
}
|
|
@@ -152,6 +205,7 @@ export function serializeLocalAwarenessIntent(
|
|
|
152
205
|
const wire = Object.create(null) as Record<string, unknown>;
|
|
153
206
|
wire.state = intent.state;
|
|
154
207
|
wire.focused = intent.focused;
|
|
208
|
+
|
|
155
209
|
if (intent.selection === null) {
|
|
156
210
|
wire.selection = null;
|
|
157
211
|
} else if (intent.selection !== undefined) {
|
|
@@ -161,6 +215,7 @@ export function serializeLocalAwarenessIntent(
|
|
|
161
215
|
selection.head = intent.selection.head;
|
|
162
216
|
wire.selection = selection;
|
|
163
217
|
}
|
|
218
|
+
|
|
164
219
|
return serializeV2CreateEnvelope(wire);
|
|
165
220
|
} catch {
|
|
166
221
|
invalidLocalAwarenessIntent();
|