@pm-cm/yjs 0.0.1

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 ADDED
@@ -0,0 +1,164 @@
1
+ # @pm-cm/yjs
2
+
3
+ Collaborative split-editor sync between ProseMirror and CodeMirror over Yjs.
4
+
5
+ Extends `@pm-cm/core` with real-time collaboration: synchronizes a ProseMirror `XmlFragment` and a CodeMirror-friendly `Y.Text` through a single Yjs `Doc`, with collaborative cursor support. The serialization format is pluggable — you provide `serialize` and `parse` functions.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ npm install @pm-cm/yjs @pm-cm/core yjs y-prosemirror y-protocols prosemirror-model prosemirror-state prosemirror-view
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### 1. Document sync (Bridge)
16
+
17
+ Keeps `Y.Text` (CodeMirror) and `Y.XmlFragment` (ProseMirror) in sync within a single Yjs doc.
18
+
19
+ ```ts
20
+ import { Doc } from 'yjs'
21
+ import { createYjsBridge } from '@pm-cm/yjs'
22
+
23
+ const doc = new Doc()
24
+
25
+ const bridge = createYjsBridge({
26
+ doc,
27
+ sharedText: doc.getText('text'),
28
+ sharedProseMirror: doc.getXmlFragment('prosemirror'),
29
+ schema, // ProseMirror Schema
30
+ serialize, // (doc: Node) => string
31
+ parse, // (text: string, schema: Schema) => Node
32
+ })
33
+
34
+ // bootstrapResult tells you how the bridge initialized
35
+ console.log(bridge.bootstrapResult.source) // 'text' | 'prosemirror' | 'both-match' | 'empty' | 'initial'
36
+
37
+ // After a ProseMirror edit, push to the CodeMirror side:
38
+ bridge.syncToSharedText(editorView.state.doc)
39
+
40
+ // CodeMirror → ProseMirror direction is automatic (Y.Text observer).
41
+
42
+ bridge.dispose() // cleanup
43
+ ```
44
+
45
+ ### 2. Collaborative editing with cursor sync
46
+
47
+ `createCollabPlugins` returns ProseMirror plugins for Yjs sync, remote cursors, and undo. Pass `serialize` and `cursorSync: true` to additionally enable PM ↔ CM cursor synchronization.
48
+
49
+ ```ts
50
+ import { EditorState } from 'prosemirror-state'
51
+ import { EditorView } from 'prosemirror-view'
52
+ import { Awareness } from 'y-protocols/awareness'
53
+ import { Doc } from 'yjs'
54
+ import { createYjsBridge, createCollabPlugins, syncCmCursor } from '@pm-cm/yjs'
55
+
56
+ const doc = new Doc()
57
+ const awareness = new Awareness(doc)
58
+ const xmlFragment = doc.getXmlFragment('prosemirror')
59
+
60
+ const bridge = createYjsBridge({
61
+ doc,
62
+ sharedText: doc.getText('text'),
63
+ sharedProseMirror: xmlFragment,
64
+ schema,
65
+ serialize,
66
+ parse,
67
+ })
68
+
69
+ // Setup ProseMirror with collab + cursor sync plugins
70
+ const { plugins, doc: pmDoc } = createCollabPlugins(schema, {
71
+ sharedProseMirror: xmlFragment,
72
+ awareness,
73
+ serialize,
74
+ cursorSync: true, // opt-in (default false)
75
+ bridge, // auto-wires PM doc changes → Y.Text
76
+ })
77
+
78
+ const view = new EditorView(element, {
79
+ state: EditorState.create({ schema, doc: pmDoc, plugins }),
80
+ })
81
+
82
+ // PM cursor → awareness is automatic (plugin handles it).
83
+
84
+ // CM cursor → awareness: one line.
85
+ syncCmCursor(view, cmOffset)
86
+ ```
87
+
88
+ ## API
89
+
90
+ ### Bridge
91
+
92
+ | Export | Description |
93
+ |---|---|
94
+ | `createYjsBridge(config, options?)` | Create a bridge that keeps `Y.Text` (CM) and `Y.XmlFragment` (PM) in sync. Throws if shared types are detached or belong to a different `Y.Doc` |
95
+ | `replaceSharedText(sharedText, next, origin, normalize?)` | Minimal-diff replace of `Y.Text` content. Returns `ReplaceTextResult` |
96
+ | `replaceSharedProseMirror(doc, fragment, text, origin, config)` | Replace `Y.XmlFragment` from serialized text. Returns `ReplaceProseMirrorResult` |
97
+
98
+ ### Bridge Sync Plugin
99
+
100
+ | Export | Description |
101
+ |---|---|
102
+ | `createBridgeSyncPlugin(bridge, options?)` | ProseMirror plugin that auto-wires PM doc changes → `bridge.syncToSharedText()` |
103
+ | `bridgeSyncPluginKey` | `PluginKey` for the bridge sync plugin |
104
+
105
+ ### Collab Plugins
106
+
107
+ | Export | Description |
108
+ |---|---|
109
+ | `createCollabPlugins(schema, options)` | Returns `{ plugins, doc, mapping }`. Bundles `ySyncPlugin`, `yCursorPlugin`, `yUndoPlugin`, and optionally cursor sync. `cursorSync` defaults to `false`. **Throws** if `cursorSync: true` but `serialize` is not provided |
110
+ | `createAwarenessProxy(awareness, cursorFieldName?)` | Proxy that suppresses y-prosemirror's auto cursor management |
111
+
112
+ ### Cursor Sync
113
+
114
+ | Export | Description |
115
+ |---|---|
116
+ | `createCursorSyncPlugin(options)` | ProseMirror plugin: PM selection → awareness (auto), CM offset → awareness (via meta) |
117
+ | `syncCmCursor(view, anchor, head?, onWarning?)` | Dispatch a CM cursor offset (or range) to the cursor sync plugin. Input values are floored to non-negative integers |
118
+ | `cursorSyncPluginKey` | `PluginKey<CursorSyncState>` for reading plugin state |
119
+
120
+ ### Cursor Mapping (re-exported from @pm-cm/core)
121
+
122
+ | Export | Description |
123
+ |---|---|
124
+ | `buildCursorMap(doc, serialize, locate?)` | Build a PM position ↔ CM offset mapping |
125
+ | `cursorMapLookup(map, pmPos)` | PM position → CM offset |
126
+ | `reverseCursorMapLookup(map, cmOffset)` | CM offset → PM position |
127
+
128
+ ### Constants
129
+
130
+ | Export | Value |
131
+ |---|---|
132
+ | `ORIGIN_TEXT_TO_PM` | `'bridge:text-to-prosemirror'` |
133
+ | `ORIGIN_PM_TO_TEXT` | `'bridge:prosemirror-to-text'` |
134
+ | `ORIGIN_INIT` | `'bridge:init'` |
135
+
136
+ ### Types
137
+
138
+ | Type | Description |
139
+ |---|---|
140
+ | `YjsBridgeConfig` | Config for `createYjsBridge` — uses `sharedProseMirror` (capital M) |
141
+ | `YjsBridgeHandle` | Handle returned by `createYjsBridge` — includes `bootstrapResult` |
142
+ | `YjsBridgeOptions` | Options for `createYjsBridge` (`initialText?`, `prefer?`) |
143
+ | `BootstrapResult` | `{ source: 'text' \| 'prosemirror' \| 'both-match' \| 'empty' \| 'initial', parseError?: boolean }` |
144
+ | `CollabPluginsOptions` | Options for `createCollabPlugins` — uses `sharedProseMirror` (capital M) |
145
+ | `CursorSyncPluginOptions` | Options for `createCursorSyncPlugin` |
146
+ | `CursorSyncState` | Plugin state: `{ pendingCm, mappedTextOffset }` |
147
+ | `ReplaceResult` | Union: `ReplaceTextResult \| ReplaceProseMirrorResult` |
148
+ | `ReplaceTextResult` | `{ ok: true } \| { ok: false; reason: 'unchanged' \| 'detached' }` |
149
+ | `ReplaceProseMirrorResult` | `{ ok: true } \| { ok: false; reason: 'parse-error' \| 'detached' }` |
150
+ | `YCursorPluginOpts` | Typed options for `yCursorPlugin` (`awarenessStateFilter`, `cursorBuilder`, `selectionBuilder`, `getSelection`) |
151
+ | `YUndoPluginOpts` | Typed options for `yUndoPlugin` (`protectedNodes`, `trackedOrigins`, `undoManager`) |
152
+ | `ProseMirrorMapping` | `Map<AbstractType<unknown>, Node \| Node[]>` — Yjs ↔ PM node mapping |
153
+ | `ErrorCode` | `'parse-error' \| 'serialize-error'` — re-exported from `@pm-cm/core` |
154
+ | `ErrorEvent` | `{ code: ErrorCode, message: string, cause: unknown }` — structured error event |
155
+ | `OnError` | `(event: ErrorEvent) => void` — re-exported from `@pm-cm/core` |
156
+ | `WarningCode` | `'bridge-already-wired' \| 'sync-failed' \| 'ysync-plugin-missing' \| 'cursor-sync-not-installed'` |
157
+ | `WarningEvent` | `{ code: WarningCode, message: string }` — structured warning event |
158
+ | `OnWarning` | `(event: WarningEvent) => void` — warning handler callback |
159
+ | `Serialize`, `Parse`, `Normalize` | Re-exported from `@pm-cm/core` |
160
+ | `TextSegment`, `CursorMap`, `LocateText` | Re-exported from `@pm-cm/core` |
161
+
162
+ ### Compatibility
163
+
164
+ This package depends on internal state shapes of `y-prosemirror` (specifically `ySyncPluginKey` state and transaction meta). These internals are accessed via runtime type guards that degrade gracefully (returning `null` / logging a warning) when the expected shape is absent. The tested and supported version is `y-prosemirror@^1.3.7`.
package/dist/index.cjs ADDED
@@ -0,0 +1,515 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ORIGIN_INIT: () => ORIGIN_INIT,
24
+ ORIGIN_PM_TO_TEXT: () => ORIGIN_PM_TO_TEXT,
25
+ ORIGIN_TEXT_TO_PM: () => ORIGIN_TEXT_TO_PM,
26
+ bridgeSyncPluginKey: () => bridgeSyncPluginKey,
27
+ buildCursorMap: () => import_core2.buildCursorMap,
28
+ createAwarenessProxy: () => createAwarenessProxy,
29
+ createBridgeSyncPlugin: () => createBridgeSyncPlugin,
30
+ createCollabPlugins: () => createCollabPlugins,
31
+ createCursorSyncPlugin: () => createCursorSyncPlugin,
32
+ createYjsBridge: () => createYjsBridge,
33
+ cursorMapLookup: () => import_core2.cursorMapLookup,
34
+ cursorSyncPluginKey: () => cursorSyncPluginKey,
35
+ replaceSharedProseMirror: () => replaceSharedProseMirror,
36
+ replaceSharedText: () => replaceSharedText,
37
+ reverseCursorMapLookup: () => import_core2.reverseCursorMapLookup,
38
+ syncCmCursor: () => syncCmCursor
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/bridge.ts
43
+ var import_y_prosemirror = require("y-prosemirror");
44
+
45
+ // src/types.ts
46
+ var ORIGIN_TEXT_TO_PM = "bridge:text-to-prosemirror";
47
+ var ORIGIN_PM_TO_TEXT = "bridge:prosemirror-to-text";
48
+ var ORIGIN_INIT = "bridge:init";
49
+
50
+ // src/bridge.ts
51
+ var defaultNormalize = (s) => s.replace(/\r\n?/g, "\n");
52
+ var defaultOnError = (event) => console.error(`[bridge] ${event.code}: ${event.message}`, event.cause);
53
+ function replaceSharedText(sharedText, next, origin, normalize = defaultNormalize) {
54
+ if (!sharedText.doc) {
55
+ return { ok: false, reason: "detached" };
56
+ }
57
+ const normalized = normalize(next);
58
+ const current = sharedText.toString();
59
+ if (current === normalized) {
60
+ return { ok: false, reason: "unchanged" };
61
+ }
62
+ let start = 0;
63
+ const minLen = Math.min(current.length, normalized.length);
64
+ while (start < minLen && current.charCodeAt(start) === normalized.charCodeAt(start)) {
65
+ start++;
66
+ }
67
+ let endCurrent = current.length;
68
+ let endNext = normalized.length;
69
+ while (endCurrent > start && endNext > start && current.charCodeAt(endCurrent - 1) === normalized.charCodeAt(endNext - 1)) {
70
+ endCurrent--;
71
+ endNext--;
72
+ }
73
+ sharedText.doc.transact(() => {
74
+ const deleteCount = endCurrent - start;
75
+ if (deleteCount > 0) {
76
+ sharedText.delete(start, deleteCount);
77
+ }
78
+ const insertStr = normalized.slice(start, endNext);
79
+ if (insertStr.length > 0) {
80
+ sharedText.insert(start, insertStr);
81
+ }
82
+ }, origin);
83
+ return { ok: true };
84
+ }
85
+ function replaceSharedProseMirror(doc, fragment, text, origin, config) {
86
+ if (!fragment.doc) {
87
+ return { ok: false, reason: "detached" };
88
+ }
89
+ if (fragment.doc !== doc) {
90
+ throw new Error("fragment belongs to a different Y.Doc than the provided doc");
91
+ }
92
+ const normalize = config.normalize ?? defaultNormalize;
93
+ const onError = config.onError ?? defaultOnError;
94
+ let nextDoc;
95
+ try {
96
+ nextDoc = config.parse(normalize(text), config.schema);
97
+ } catch (error) {
98
+ onError({ code: "parse-error", message: "failed to parse text into ProseMirror document", cause: error });
99
+ return { ok: false, reason: "parse-error" };
100
+ }
101
+ doc.transact(() => {
102
+ (0, import_y_prosemirror.prosemirrorToYXmlFragment)(nextDoc, fragment);
103
+ }, origin);
104
+ return { ok: true };
105
+ }
106
+ function createYjsBridge(config, options) {
107
+ const {
108
+ doc,
109
+ sharedText,
110
+ sharedProseMirror,
111
+ schema,
112
+ serialize,
113
+ parse
114
+ } = config;
115
+ const normalize = config.normalize ?? defaultNormalize;
116
+ const onError = config.onError ?? defaultOnError;
117
+ if (!sharedText.doc) {
118
+ throw new Error("sharedText is not attached to any Y.Doc");
119
+ }
120
+ if (sharedText.doc !== doc) {
121
+ throw new Error("sharedText belongs to a different Y.Doc than the provided doc");
122
+ }
123
+ if (!sharedProseMirror.doc) {
124
+ throw new Error("sharedProseMirror is not attached to any Y.Doc");
125
+ }
126
+ if (sharedProseMirror.doc !== doc) {
127
+ throw new Error("sharedProseMirror belongs to a different Y.Doc than the provided doc");
128
+ }
129
+ let lastBridgedText = null;
130
+ const syncTextToProsemirror = (origin) => {
131
+ const text = normalize(sharedText.toString());
132
+ if (lastBridgedText === text) {
133
+ return true;
134
+ }
135
+ const result = replaceSharedProseMirror(doc, sharedProseMirror, text, origin, {
136
+ schema,
137
+ parse,
138
+ normalize,
139
+ onError
140
+ });
141
+ if (result.ok) {
142
+ lastBridgedText = text;
143
+ }
144
+ return result.ok;
145
+ };
146
+ const sharedProseMirrorToText = (fragment) => {
147
+ try {
148
+ const pmDoc = (0, import_y_prosemirror.yXmlFragmentToProseMirrorRootNode)(fragment, schema);
149
+ return normalize(serialize(pmDoc));
150
+ } catch (error) {
151
+ onError({ code: "serialize-error", message: "failed to convert ProseMirror fragment to text", cause: error });
152
+ return null;
153
+ }
154
+ };
155
+ const bootstrap = () => {
156
+ const text = normalize(sharedText.toString());
157
+ const hasText = text.length > 0;
158
+ const hasProsemirror = sharedProseMirror.length > 0;
159
+ if (!hasText && !hasProsemirror) {
160
+ const initial = options?.initialText ?? "";
161
+ if (initial.length > 0) {
162
+ const initResult = replaceSharedProseMirror(doc, sharedProseMirror, initial, ORIGIN_INIT, {
163
+ schema,
164
+ parse,
165
+ normalize,
166
+ onError
167
+ });
168
+ if (!initResult.ok) {
169
+ return { source: "initial", parseError: true };
170
+ }
171
+ const pmDoc = (0, import_y_prosemirror.yXmlFragmentToProseMirrorRootNode)(sharedProseMirror, schema);
172
+ const canonicalText = serialize(pmDoc);
173
+ replaceSharedText(sharedText, canonicalText, ORIGIN_INIT, normalize);
174
+ lastBridgedText = normalize(canonicalText);
175
+ return { source: "initial" };
176
+ }
177
+ return { source: "empty" };
178
+ }
179
+ if (hasText && !hasProsemirror) {
180
+ const ok = syncTextToProsemirror(ORIGIN_INIT);
181
+ return { source: "text", ...!ok && { parseError: true } };
182
+ }
183
+ if (!hasText && hasProsemirror) {
184
+ const textFromProsemirror = sharedProseMirrorToText(sharedProseMirror);
185
+ if (textFromProsemirror !== null) {
186
+ replaceSharedText(sharedText, textFromProsemirror, ORIGIN_INIT, normalize);
187
+ lastBridgedText = normalize(textFromProsemirror);
188
+ return { source: "prosemirror" };
189
+ }
190
+ return { source: "prosemirror", parseError: true };
191
+ }
192
+ const prosemirrorText = sharedProseMirrorToText(sharedProseMirror);
193
+ if (prosemirrorText === null) {
194
+ const fallbackText = hasText ? text : options?.initialText ?? "";
195
+ let parseError = false;
196
+ if (fallbackText.length > 0) {
197
+ replaceSharedText(sharedText, fallbackText, ORIGIN_INIT, normalize);
198
+ const fallbackResult = replaceSharedProseMirror(doc, sharedProseMirror, fallbackText, ORIGIN_INIT, {
199
+ schema,
200
+ parse,
201
+ normalize,
202
+ onError
203
+ });
204
+ if (!fallbackResult.ok) parseError = true;
205
+ }
206
+ return { source: "text", ...parseError && { parseError: true } };
207
+ }
208
+ if (prosemirrorText !== text) {
209
+ const prefer = options?.prefer ?? "text";
210
+ if (prefer === "prosemirror") {
211
+ replaceSharedText(sharedText, prosemirrorText, ORIGIN_INIT, normalize);
212
+ lastBridgedText = normalize(prosemirrorText);
213
+ return { source: "prosemirror" };
214
+ } else {
215
+ const ok = syncTextToProsemirror(ORIGIN_INIT);
216
+ return { source: "text", ...!ok && { parseError: true } };
217
+ }
218
+ } else {
219
+ lastBridgedText = text;
220
+ return { source: "both-match" };
221
+ }
222
+ };
223
+ const textObserver = (_, transaction) => {
224
+ if (transaction.origin === ORIGIN_PM_TO_TEXT || transaction.origin === ORIGIN_INIT) {
225
+ return;
226
+ }
227
+ syncTextToProsemirror(ORIGIN_TEXT_TO_PM);
228
+ };
229
+ const bootstrapResult = bootstrap();
230
+ sharedText.observe(textObserver);
231
+ return {
232
+ bootstrapResult,
233
+ syncToSharedText(doc2) {
234
+ const text = serialize(doc2);
235
+ const result = replaceSharedText(sharedText, text, ORIGIN_PM_TO_TEXT, normalize);
236
+ if (result.ok || result.reason === "unchanged") {
237
+ lastBridgedText = normalize(text);
238
+ }
239
+ return result;
240
+ },
241
+ isYjsSyncChange(tr) {
242
+ const meta = tr.getMeta(import_y_prosemirror.ySyncPluginKey);
243
+ return typeof meta === "object" && meta !== null && "isChangeOrigin" in meta && meta.isChangeOrigin === true;
244
+ },
245
+ dispose() {
246
+ sharedText.unobserve(textObserver);
247
+ }
248
+ };
249
+ }
250
+
251
+ // src/awareness-proxy.ts
252
+ function createAwarenessProxy(awareness, cursorField = "pmCursor") {
253
+ return new Proxy(awareness, {
254
+ get(target, prop, receiver) {
255
+ if (prop === "getLocalState") {
256
+ return () => {
257
+ const state = target.getLocalState();
258
+ return state ? { ...state, [cursorField]: null } : state;
259
+ };
260
+ }
261
+ if (prop === "setLocalStateField") {
262
+ return (field, value2) => {
263
+ if (field === cursorField) return;
264
+ target.setLocalStateField(field, value2);
265
+ };
266
+ }
267
+ const value = Reflect.get(target, prop, receiver);
268
+ return typeof value === "function" ? value.bind(target) : value;
269
+ }
270
+ });
271
+ }
272
+
273
+ // src/collab-plugins.ts
274
+ var import_y_prosemirror3 = require("y-prosemirror");
275
+
276
+ // src/bridge-sync-plugin.ts
277
+ var import_prosemirror_state = require("prosemirror-state");
278
+ var bridgeSyncPluginKey = new import_prosemirror_state.PluginKey("pm-cm-bridge-sync");
279
+ var wiredBridges = /* @__PURE__ */ new WeakSet();
280
+ var defaultOnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`);
281
+ function createBridgeSyncPlugin(bridge, options = {}) {
282
+ const warn = options.onWarning ?? defaultOnWarning;
283
+ if (wiredBridges.has(bridge)) {
284
+ warn({ code: "bridge-already-wired", message: "this bridge is already wired to another plugin instance" });
285
+ }
286
+ wiredBridges.add(bridge);
287
+ return new import_prosemirror_state.Plugin({
288
+ key: bridgeSyncPluginKey,
289
+ state: {
290
+ init() {
291
+ return { needsSync: false };
292
+ },
293
+ apply(tr, _prev) {
294
+ if (!tr.docChanged) return { needsSync: false };
295
+ if (bridge.isYjsSyncChange(tr)) return { needsSync: false };
296
+ return { needsSync: true };
297
+ }
298
+ },
299
+ view() {
300
+ return {
301
+ update(view) {
302
+ const state = bridgeSyncPluginKey.getState(view.state);
303
+ if (state?.needsSync) {
304
+ const result = bridge.syncToSharedText(view.state.doc);
305
+ if (!result.ok) {
306
+ if (result.reason === "detached") {
307
+ options.onSyncFailure?.(result, view);
308
+ warn({ code: "sync-failed", message: `bridge sync failed: ${result.reason}` });
309
+ }
310
+ }
311
+ }
312
+ },
313
+ destroy() {
314
+ wiredBridges.delete(bridge);
315
+ }
316
+ };
317
+ }
318
+ });
319
+ }
320
+
321
+ // src/cursor-sync-plugin.ts
322
+ var import_prosemirror_state2 = require("prosemirror-state");
323
+ var import_y_prosemirror2 = require("y-prosemirror");
324
+ var import_yjs = require("yjs");
325
+ var import_core = require("@pm-cm/core");
326
+ var cursorSyncPluginKey = new import_prosemirror_state2.PluginKey("pm-cm-cursor-sync");
327
+ function getYSyncState(view) {
328
+ const raw = import_y_prosemirror2.ySyncPluginKey.getState(view.state);
329
+ if (!raw) return null;
330
+ if (typeof raw === "object" && "type" in raw && raw.type && "binding" in raw && raw.binding && typeof raw.binding === "object" && "mapping" in raw.binding && raw.binding.mapping instanceof Map) {
331
+ return raw;
332
+ }
333
+ return null;
334
+ }
335
+ function toRelativePosition(view, pmPos) {
336
+ const ySyncState = getYSyncState(view);
337
+ if (!ySyncState) return null;
338
+ return (0, import_y_prosemirror2.absolutePositionToRelativePosition)(
339
+ pmPos,
340
+ ySyncState.type,
341
+ ySyncState.binding.mapping
342
+ // eslint-disable-line @typescript-eslint/no-explicit-any -- y-prosemirror internal mapping type
343
+ );
344
+ }
345
+ function broadcastPmCursor(awareness, cursorFieldName, view, pmAnchor, pmHead) {
346
+ const relAnchor = toRelativePosition(view, pmAnchor);
347
+ const relHead = toRelativePosition(view, pmHead);
348
+ if (relAnchor === null || relHead === null) return false;
349
+ awareness.setLocalStateField(cursorFieldName, { anchor: relAnchor, head: relHead });
350
+ return true;
351
+ }
352
+ function broadcastTextCursor(awareness, cmCursorFieldName, sharedText, textAnchor, textHead) {
353
+ const len = sharedText.length;
354
+ const clamp = (v) => Math.max(0, Math.min(v, len));
355
+ const relAnchor = (0, import_yjs.createRelativePositionFromTypeIndex)(sharedText, clamp(textAnchor));
356
+ const relHead = (0, import_yjs.createRelativePositionFromTypeIndex)(sharedText, clamp(textHead));
357
+ awareness.setLocalStateField(cmCursorFieldName, { anchor: relAnchor, head: relHead });
358
+ }
359
+ var defaultOnWarning2 = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`);
360
+ function createCursorSyncPlugin(options) {
361
+ const { awareness, serialize, locate, sharedText } = options;
362
+ const warn = options.onWarning ?? defaultOnWarning2;
363
+ const cursorFieldName = options.cursorFieldName ?? "pmCursor";
364
+ const cmCursorFieldName = options.cmCursorFieldName ?? "cursor";
365
+ let warnedSyncPluginMissing = false;
366
+ let cachedMap = null;
367
+ let cachedMapDoc = null;
368
+ function getOrBuildMap(doc) {
369
+ if (cachedMapDoc !== doc || !cachedMap) {
370
+ cachedMap = (0, import_core.buildCursorMap)(doc, serialize, locate);
371
+ cachedMapDoc = doc;
372
+ }
373
+ return cachedMap;
374
+ }
375
+ return new import_prosemirror_state2.Plugin({
376
+ key: cursorSyncPluginKey,
377
+ state: {
378
+ init() {
379
+ return { pendingCm: null, mappedTextOffset: null };
380
+ },
381
+ apply(tr, prev, _oldState, newState) {
382
+ const cmMeta = tr.getMeta(cursorSyncPluginKey);
383
+ if (cmMeta) {
384
+ return { pendingCm: cmMeta, mappedTextOffset: prev.mappedTextOffset };
385
+ }
386
+ let mappedTextOffset = prev.mappedTextOffset;
387
+ if (tr.selectionSet || tr.docChanged) {
388
+ const map = getOrBuildMap(newState.doc);
389
+ mappedTextOffset = (0, import_core.cursorMapLookup)(map, newState.selection.anchor);
390
+ }
391
+ return {
392
+ pendingCm: prev.pendingCm !== null ? null : prev.pendingCm,
393
+ mappedTextOffset
394
+ };
395
+ }
396
+ },
397
+ view() {
398
+ return {
399
+ update(view, prevState) {
400
+ const pluginState = cursorSyncPluginKey.getState(view.state);
401
+ const prevPluginState = cursorSyncPluginKey.getState(prevState);
402
+ if (pluginState?.pendingCm != null && pluginState.pendingCm !== prevPluginState?.pendingCm) {
403
+ const map = getOrBuildMap(view.state.doc);
404
+ const pmAnchor = (0, import_core.reverseCursorMapLookup)(map, pluginState.pendingCm.anchor);
405
+ const pmHead = (0, import_core.reverseCursorMapLookup)(map, pluginState.pendingCm.head);
406
+ if (pmAnchor !== null && pmHead !== null) {
407
+ const ok = broadcastPmCursor(awareness, cursorFieldName, view, pmAnchor, pmHead);
408
+ if (!ok && !warnedSyncPluginMissing) {
409
+ warnedSyncPluginMissing = true;
410
+ warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
411
+ }
412
+ }
413
+ if (sharedText) {
414
+ broadcastTextCursor(
415
+ awareness,
416
+ cmCursorFieldName,
417
+ sharedText,
418
+ pluginState.pendingCm.anchor,
419
+ pluginState.pendingCm.head
420
+ );
421
+ }
422
+ return;
423
+ }
424
+ if (view.hasFocus() && (view.state.selection !== prevState.selection || view.state.doc !== prevState.doc)) {
425
+ const { anchor, head } = view.state.selection;
426
+ const ok = broadcastPmCursor(awareness, cursorFieldName, view, anchor, head);
427
+ if (!ok && !warnedSyncPluginMissing) {
428
+ warnedSyncPluginMissing = true;
429
+ warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
430
+ }
431
+ if (sharedText) {
432
+ const map = getOrBuildMap(view.state.doc);
433
+ const textAnchor = (0, import_core.cursorMapLookup)(map, anchor);
434
+ const textHead = (0, import_core.cursorMapLookup)(map, head);
435
+ if (textAnchor !== null && textHead !== null) {
436
+ broadcastTextCursor(awareness, cmCursorFieldName, sharedText, textAnchor, textHead);
437
+ }
438
+ }
439
+ }
440
+ }
441
+ };
442
+ }
443
+ });
444
+ }
445
+ function syncCmCursor(view, anchor, head, onWarning) {
446
+ if (!cursorSyncPluginKey.getState(view.state)) {
447
+ (onWarning ?? defaultOnWarning2)({ code: "cursor-sync-not-installed", message: "cursor sync plugin is not installed on this EditorView" });
448
+ return;
449
+ }
450
+ const sanitize = (v) => Math.max(0, Math.floor(v));
451
+ view.dispatch(
452
+ view.state.tr.setMeta(cursorSyncPluginKey, {
453
+ anchor: sanitize(anchor),
454
+ head: sanitize(head ?? anchor)
455
+ })
456
+ );
457
+ }
458
+
459
+ // src/collab-plugins.ts
460
+ function createCollabPlugins(schema, options) {
461
+ const cursorFieldName = options.cursorFieldName ?? "pmCursor";
462
+ const enableCursorSync = options.cursorSync ?? false;
463
+ const { sharedProseMirror } = options;
464
+ if (enableCursorSync && !options.serialize) {
465
+ throw new Error("createCollabPlugins: cursorSync requires serialize to be provided");
466
+ }
467
+ const { doc, mapping: rawMapping } = (0, import_y_prosemirror3.initProseMirrorDoc)(sharedProseMirror, schema);
468
+ const mapping = rawMapping;
469
+ const pmAwareness = enableCursorSync ? createAwarenessProxy(options.awareness, cursorFieldName) : options.awareness;
470
+ const plugins = [
471
+ (0, import_y_prosemirror3.ySyncPlugin)(sharedProseMirror, { mapping: rawMapping }),
472
+ (0, import_y_prosemirror3.yCursorPlugin)(pmAwareness, options.yCursorPluginOpts ?? {}, cursorFieldName),
473
+ (0, import_y_prosemirror3.yUndoPlugin)(options.yUndoPluginOpts)
474
+ ];
475
+ if (options.bridge) {
476
+ plugins.push(createBridgeSyncPlugin(options.bridge, { onWarning: options.onWarning }));
477
+ }
478
+ if (enableCursorSync && options.serialize) {
479
+ plugins.push(
480
+ createCursorSyncPlugin({
481
+ awareness: options.awareness,
482
+ serialize: options.serialize,
483
+ cursorFieldName,
484
+ cmCursorFieldName: options.cmCursorFieldName,
485
+ locate: options.locate,
486
+ sharedText: options.sharedText,
487
+ onWarning: options.onWarning
488
+ })
489
+ );
490
+ }
491
+ return { plugins, doc, mapping };
492
+ }
493
+
494
+ // src/index.ts
495
+ var import_core2 = require("@pm-cm/core");
496
+ // Annotate the CommonJS export names for ESM import in node:
497
+ 0 && (module.exports = {
498
+ ORIGIN_INIT,
499
+ ORIGIN_PM_TO_TEXT,
500
+ ORIGIN_TEXT_TO_PM,
501
+ bridgeSyncPluginKey,
502
+ buildCursorMap,
503
+ createAwarenessProxy,
504
+ createBridgeSyncPlugin,
505
+ createCollabPlugins,
506
+ createCursorSyncPlugin,
507
+ createYjsBridge,
508
+ cursorMapLookup,
509
+ cursorSyncPluginKey,
510
+ replaceSharedProseMirror,
511
+ replaceSharedText,
512
+ reverseCursorMapLookup,
513
+ syncCmCursor
514
+ });
515
+ //# sourceMappingURL=index.cjs.map