@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 +164 -0
- package/dist/index.cjs +515 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +259 -0
- package/dist/index.d.ts +259 -0
- package/dist/index.js +473 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
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
|