@kerebron/extension-yjs 0.1.3 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"CoreEditor.d.ts","sourceRoot":"","sources":["../../../src/editor/src/CoreEditor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,IAAI,IAAI,eAAe,EAAE,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGzE,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAkB,MAAM,8BAA8B,CAAC;AAgC/E,qBAAa,UAAW,SAAQ,WAAW;IACzC,SAAgB,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,CAG7C;IACF,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAiB;IAChC,IAAI,EAAG,UAAU,CAAC;IAClB,KAAK,EAAG,WAAW,CAAC;gBAEf,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM;IAyBhD,IAAW,MAAM,qBAEhB;IAEM,KAAK,IAAI,eAAe;IAIxB,GAAG,IAAI,eAAe;IAI7B,OAAO,CAAC,UAAU;IAaX,mBAAmB,CAAC,WAAW,EAAE,WAAW;IAcnD,OAAO,CAAC,YAAY;IAcb,WAAW,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,SAAS,CAAC,EAAE,MAAM;IAwC7C,WAAW,CAAC,SAAS,CAAC,EAAE,MAAM;IAkB9B,OAAO,IAAI,WAAW;IAItB,KAAK,CAAC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GAAG,UAAU;IAOvD,KAAK,CAAC,GAAG,CAAC,EAAE,eAAe;CAMnC"}
1
+ {"version":3,"file":"CoreEditor.d.ts","sourceRoot":"","sources":["../../../src/editor/src/CoreEditor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,IAAI,IAAI,eAAe,EAAE,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGzE,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAkB,MAAM,8BAA8B,CAAC;AAgC/E,qBAAa,UAAW,SAAQ,WAAW;IACzC,SAAgB,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,CAG7C;IACF,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAiB;IAChC,IAAI,EAAG,UAAU,CAAC;IAClB,KAAK,EAAG,WAAW,CAAC;gBAEf,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM;IAyBhD,IAAW,MAAM,qBAEhB;IAEM,KAAK,IAAI,eAAe;IAIxB,GAAG,IAAI,eAAe;IAI7B,OAAO,CAAC,UAAU;IAgBX,mBAAmB,CAAC,WAAW,EAAE,WAAW;IAcnD,OAAO,CAAC,YAAY;IAcb,WAAW,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,SAAS,CAAC,EAAE,MAAM;IAwC7C,WAAW,CAAC,SAAS,CAAC,EAAE,MAAM;IAkB9B,OAAO,IAAI,WAAW;IAItB,KAAK,CAAC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GAAG,UAAU;IAOvD,KAAK,CAAC,GAAG,CAAC,EAAE,eAAe;CAMnC"}
@@ -99,6 +99,9 @@ export class CoreEditor extends EventTarget {
99
99
  if (this.options.element) {
100
100
  this.view = new EditorView(this.options.element, {
101
101
  state: this.state,
102
+ attributes: {
103
+ class: 'kb-editor',
104
+ },
102
105
  dispatchTransaction: (tx) => this.dispatchTransaction(tx),
103
106
  });
104
107
  }
@@ -1,6 +1,6 @@
1
1
  import { Extension } from '../../editor/src/mod.js';
2
2
  import { initProseMirrorDoc, redo, undo, yUndoPlugin } from 'y-prosemirror';
3
- import { ySyncPlugin } from 'y-prosemirror';
3
+ import { ySyncPlugin } from './ySyncPlugin.js';
4
4
  import { yCursorPlugin } from './yCursorPlugin.js';
5
5
  export class ExtensionYjs extends Extension {
6
6
  constructor() {
@@ -0,0 +1,5 @@
1
+ export declare const userColors: {
2
+ color: string;
3
+ light: string;
4
+ }[];
5
+ //# sourceMappingURL=userColors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userColors.d.ts","sourceRoot":"","sources":["../../../src/extension-yjs/src/userColors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU;;;GAStB,CAAC"}
@@ -0,0 +1,10 @@
1
+ export const userColors = [
2
+ { color: '#30bced', light: '#30bced33' },
3
+ { color: '#6eeb83', light: '#6eeb8333' },
4
+ { color: '#ffbc42', light: '#ffbc4233' },
5
+ { color: '#ecd444', light: '#ecd44433' },
6
+ { color: '#ee6352', light: '#ee635233' },
7
+ { color: '#9ac2c9', light: '#9ac2c933' },
8
+ { color: '#8acb88', light: '#8acb8833' },
9
+ { color: '#1be7ff', light: '#1be7ff33' },
10
+ ];
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @param {any} json
3
+ */
4
+ export declare const hashOfJSON: (json: any) => string;
5
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/extension-yjs/src/utils.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,eAAO,MAAM,UAAU,uBACuC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import * as sha256 from 'lib0/hash/sha256';
2
+ import * as buf from 'lib0/buffer';
3
+ /**
4
+ * Custom function to transform sha256 hash to N byte
5
+ *
6
+ * @param {Uint8Array} digest
7
+ */
8
+ const _convolute = (digest) => {
9
+ const N = 6;
10
+ for (let i = N; i < digest.length; i++) {
11
+ digest[i % N] = digest[i % N] ^ digest[i];
12
+ }
13
+ return digest.slice(0, N);
14
+ };
15
+ /**
16
+ * @param {any} json
17
+ */
18
+ export const hashOfJSON = (json) => buf.toBase64(_convolute(sha256.digest(buf.encodeAny(json))));
@@ -21,8 +21,8 @@ export const defaultAwarenessStateFilter = (currentClientId, userClientId, _user
21
21
  */
22
22
  export const defaultCursorBuilder = (user) => {
23
23
  const cursor = document.createElement('span');
24
- cursor.classList.add('ProseMirror-yjs-cursor');
25
- cursor.setAttribute('style', `border-color: ${user.color}`);
24
+ cursor.classList.add('kb-yjs__cursor');
25
+ cursor.setAttribute('style', `border-color: ${user.color};`);
26
26
  const userDiv = document.createElement('div');
27
27
  userDiv.setAttribute('style', `background-color: ${user.color}`);
28
28
  userDiv.insertBefore(document.createTextNode(user.name), null);
@@ -42,7 +42,7 @@ export const defaultCursorBuilder = (user) => {
42
42
  export const defaultSelectionBuilder = (user) => {
43
43
  return {
44
44
  style: `background-color: ${user.color}70`,
45
- class: 'ProseMirror-yjs-selection',
45
+ class: 'kb-yjs__selection',
46
46
  };
47
47
  };
48
48
  const rxValidColor = /^#[0-9a-fA-F]{6}$/;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @module bindings/prosemirror
3
+ */
4
+ import { Plugin } from 'prosemirror-state';
5
+ /**
6
+ * @typedef {Object} BindingMetadata
7
+ * @property {ProsemirrorMapping} BindingMetadata.mapping
8
+ * @property {Map<import('prosemirror-model').MarkType, boolean>} BindingMetadata.isOMark - is overlapping mark
9
+ */
10
+ /**
11
+ * @return {BindingMetadata}
12
+ */
13
+ export declare const createEmptyMeta: () => {
14
+ mapping: Map<any, any>;
15
+ isOMark: Map<any, any>;
16
+ };
17
+ /**
18
+ * @param {Y.Item} item
19
+ * @param {Y.Snapshot} [snapshot]
20
+ */
21
+ export declare const isVisible: (item: any, snapshot: any) => any;
22
+ /**
23
+ * This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
24
+ *
25
+ * This plugin also keeps references to the type and the shared document so other plugins can access it.
26
+ * @param {Y.XmlFragment} yXmlFragment
27
+ * @param {YSyncOpts} opts
28
+ * @return {any} Returns a prosemirror plugin that binds to this type
29
+ */
30
+ export declare const ySyncPlugin: (yXmlFragment: any, { colors, colorMapping, permanentUserData, onFirstRender, mapping, }?: {
31
+ colors?: {
32
+ light: string;
33
+ dark: string;
34
+ }[] | undefined;
35
+ colorMapping?: Map<any, any> | undefined;
36
+ permanentUserData?: null | undefined;
37
+ onFirstRender?: (() => void) | undefined;
38
+ }) => Plugin<any>;
39
+ /**
40
+ * @param {ProsemirrorBinding} pmbinding
41
+ * @param {import('prosemirror-state').EditorState} state
42
+ */
43
+ export declare const getRelativeSelection: (pmbinding: any, state: any) => {
44
+ type: any;
45
+ anchor: any;
46
+ head: any;
47
+ };
48
+ /**
49
+ * Binding for prosemirror.
50
+ *
51
+ * @protected
52
+ */
53
+ export declare class ProsemirrorBinding {
54
+ /**
55
+ * @param {Y.XmlFragment} yXmlFragment The bind source
56
+ * @param {ProsemirrorMapping} mapping
57
+ */
58
+ constructor(yXmlFragment: any, mapping?: Map<any, any>);
59
+ /**
60
+ * Create a transaction for changing the prosemirror state.
61
+ *
62
+ * @returns
63
+ */
64
+ get _tr(): any;
65
+ _isLocalCursorInView(): any;
66
+ _isDomSelectionInView(): boolean;
67
+ /**
68
+ * @param {Y.Snapshot} snapshot
69
+ * @param {Y.Snapshot} prevSnapshot
70
+ */
71
+ renderSnapshot(snapshot: any, prevSnapshot: any): void;
72
+ unrenderSnapshot(): void;
73
+ _forceRerender(): void;
74
+ /**
75
+ * @param {Y.Snapshot|Uint8Array} snapshot
76
+ * @param {Y.Snapshot|Uint8Array} prevSnapshot
77
+ * @param {Object} pluginState
78
+ */
79
+ _renderSnapshot(snapshot: any, prevSnapshot: any, pluginState: any): void;
80
+ /**
81
+ * @param {Array<Y.YEvent<any>>} events
82
+ * @param {Y.Transaction} transaction
83
+ */
84
+ _typeChanged(events: any, transaction: any): void;
85
+ /**
86
+ * @param {import('prosemirror-model').Node} doc
87
+ */
88
+ _prosemirrorChanged(doc: any): void;
89
+ /**
90
+ * View is ready to listen to changes. Register observers.
91
+ * @param {any} prosemirrorView
92
+ */
93
+ initView(prosemirrorView: any): void;
94
+ destroy(): void;
95
+ }
96
+ /**
97
+ * @private
98
+ * @param {Y.XmlElement} el
99
+ * @param {any} schema
100
+ * @param {BindingMetadata} meta
101
+ * @param {Y.Snapshot} [snapshot]
102
+ * @param {Y.Snapshot} [prevSnapshot]
103
+ * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
104
+ * @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
105
+ */
106
+ export declare const createNodeFromYElement: (el: any, schema: any, meta: any, snapshot: any, prevSnapshot: any, computeYChange: any) => any;
107
+ /**
108
+ * @param {string} attrName
109
+ */
110
+ export declare const yattr2markname: (attrName: any) => any;
111
+ /**
112
+ * @todo move this to markstoattributes
113
+ *
114
+ * @param {Object<string, any>} attrs
115
+ * @param {import('prosemirror-model').Schema} schema
116
+ */
117
+ export declare const attributesToMarks: (attrs: any, schema: any) => any[];
118
+ /**
119
+ * Update a yDom node by syncing the current content of the prosemirror node.
120
+ *
121
+ * This is a y-prosemirror internal feature that you can use at your own risk.
122
+ *
123
+ * @private
124
+ * @unstable
125
+ *
126
+ * @param {{transact: Function}} y
127
+ * @param {Y.XmlFragment} yDomFragment
128
+ * @param {any} pNode
129
+ * @param {BindingMetadata} meta
130
+ */
131
+ export declare const updateYFragment: (y: any, yDomFragment: any, pNode: any, meta: any) => void;
132
+ //# sourceMappingURL=ySyncPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ySyncPlugin.d.ts","sourceRoot":"","sources":["../../../src/extension-yjs/src/ySyncPlugin.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAGL,MAAM,EAEP,MAAM,mBAAmB,CAAC;AAmB3B;;;;GAIG;AAEH;;GAEG;AACH,eAAO,MAAM,eAAe;;;CAG1B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,SAAS,mCAKmB,CAAC;AA8C1C;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW;;;;;;;;iBA4IvB,CAAC;AA2CF;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;CAY/B,CAAC;AAEH;;;;GAIG;AACH,qBAAa,kBAAkB;IAC7B;;;OAGG;gBACS,YAAY,KAAA,EAAE,OAAO,gBAAY;IAyC7C;;;;OAIG;IACH,IAAI,GAAG,QAEN;IAED,oBAAoB;IAYpB,qBAAqB;IA6BrB;;;OAGG;IACH,cAAc,CAAC,QAAQ,KAAA,EAAE,YAAY,KAAA;IASrC,gBAAgB;IAqBhB,cAAc;IA+Cd;;;;OAIG;IACH,eAAe,CAAC,QAAQ,KAAA,EAAE,YAAY,KAAA,EAAE,WAAW,KAAA;IAoHnD;;;OAGG;IACH,YAAY,CAAC,MAAM,KAAA,EAAE,WAAW,KAAA;IA0DhC;;OAEG;IACH,mBAAmB,CAAC,GAAG,KAAA;IAUvB;;;OAGG;IACH,QAAQ,CAAC,eAAe,KAAA;IAQxB,OAAO;CAOR;AAsCD;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,iGA2FlC,CAAC;AAiSF;;GAEG;AACH,eAAO,MAAM,cAAc,wBAC0B,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,oCAU7B,CAAC;AAyBF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,4DA2J3B,CAAC"}
@@ -0,0 +1,1092 @@
1
+ /**
2
+ * @module bindings/prosemirror
3
+ */
4
+ import { createMutex } from 'lib0/mutex';
5
+ import * as PModel from 'prosemirror-model';
6
+ import { AllSelection, NodeSelection, Plugin, TextSelection, } from 'prosemirror-state'; // eslint-disable-line
7
+ import * as math from 'lib0/math';
8
+ import * as object from 'lib0/object';
9
+ import * as set from 'lib0/set';
10
+ import { simpleDiff } from 'lib0/diff';
11
+ import * as error from 'lib0/error';
12
+ import { ySyncPluginKey, yUndoPluginKey } from 'y-prosemirror';
13
+ import * as Y from 'yjs';
14
+ import { absolutePositionToRelativePosition, relativePositionToAbsolutePosition, } from 'y-prosemirror';
15
+ import * as random from 'lib0/random';
16
+ import * as environment from 'lib0/environment';
17
+ import * as dom from 'lib0/dom';
18
+ import * as eventloop from 'lib0/eventloop';
19
+ import * as map from 'lib0/map';
20
+ import * as utils from './utils.js';
21
+ /**
22
+ * @typedef {Object} BindingMetadata
23
+ * @property {ProsemirrorMapping} BindingMetadata.mapping
24
+ * @property {Map<import('prosemirror-model').MarkType, boolean>} BindingMetadata.isOMark - is overlapping mark
25
+ */
26
+ /**
27
+ * @return {BindingMetadata}
28
+ */
29
+ export const createEmptyMeta = () => ({
30
+ mapping: new Map(),
31
+ isOMark: new Map(),
32
+ });
33
+ /**
34
+ * @param {Y.Item} item
35
+ * @param {Y.Snapshot} [snapshot]
36
+ */
37
+ export const isVisible = (item, snapshot) => snapshot === undefined
38
+ ? !item.deleted
39
+ : (snapshot.sv.has(item.id.client) && /** @type {number} */
40
+ (snapshot.sv.get(item.id.client)) > item.id.clock &&
41
+ !Y.isDeleted(snapshot.ds, item.id));
42
+ /**
43
+ * Either a node if type is YXmlElement or an Array of text nodes if YXmlText
44
+ * @typedef {Map<Y.AbstractType<any>, PModel.Node | Array<PModel.Node>>} ProsemirrorMapping
45
+ */
46
+ /**
47
+ * @typedef {Object} ColorDef
48
+ * @property {string} ColorDef.light
49
+ * @property {string} ColorDef.dark
50
+ */
51
+ /**
52
+ * @typedef {Object} YSyncOpts
53
+ * @property {Array<ColorDef>} [YSyncOpts.colors]
54
+ * @property {Map<string,ColorDef>} [YSyncOpts.colorMapping]
55
+ * @property {Y.PermanentUserData|null} [YSyncOpts.permanentUserData]
56
+ * @property {ProsemirrorMapping} [YSyncOpts.mapping]
57
+ * @property {function} [YSyncOpts.onFirstRender] Fired when the content from Yjs is initially rendered to ProseMirror
58
+ */
59
+ /**
60
+ * @type {Array<ColorDef>}
61
+ */
62
+ const defaultColors = [{ light: '#ecd44433', dark: '#ecd444' }];
63
+ /**
64
+ * @param {Map<string,ColorDef>} colorMapping
65
+ * @param {Array<ColorDef>} colors
66
+ * @param {string} user
67
+ * @return {ColorDef}
68
+ */
69
+ const getUserColor = (colorMapping, colors, user) => {
70
+ // @todo do not hit the same color twice if possible
71
+ if (!colorMapping.has(user)) {
72
+ if (colorMapping.size < colors.length) {
73
+ const usedColors = set.create();
74
+ colorMapping.forEach((color) => usedColors.add(color));
75
+ colors = colors.filter((color) => !usedColors.has(color));
76
+ }
77
+ colorMapping.set(user, random.oneOf(colors));
78
+ }
79
+ return /** @type {ColorDef} */ (colorMapping.get(user));
80
+ };
81
+ /**
82
+ * This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
83
+ *
84
+ * This plugin also keeps references to the type and the shared document so other plugins can access it.
85
+ * @param {Y.XmlFragment} yXmlFragment
86
+ * @param {YSyncOpts} opts
87
+ * @return {any} Returns a prosemirror plugin that binds to this type
88
+ */
89
+ export const ySyncPlugin = (yXmlFragment, { colors = defaultColors, colorMapping = new Map(), permanentUserData = null, onFirstRender = () => { }, mapping, } = {}) => {
90
+ let initialContentChanged = false;
91
+ const binding = new ProsemirrorBinding(yXmlFragment, mapping);
92
+ const plugin = new Plugin({
93
+ props: {
94
+ editable: (state) => {
95
+ const syncState = ySyncPluginKey.getState(state);
96
+ return syncState.snapshot == null && syncState.prevSnapshot == null;
97
+ },
98
+ },
99
+ key: ySyncPluginKey,
100
+ state: {
101
+ /**
102
+ * @returns {any}
103
+ */
104
+ init: (_initargs, _state) => {
105
+ return {
106
+ type: yXmlFragment,
107
+ doc: yXmlFragment.doc,
108
+ binding,
109
+ snapshot: null,
110
+ prevSnapshot: null,
111
+ isChangeOrigin: false,
112
+ isUndoRedoOperation: false,
113
+ addToHistory: true,
114
+ colors,
115
+ colorMapping,
116
+ permanentUserData,
117
+ };
118
+ },
119
+ apply: (tr, pluginState) => {
120
+ const change = tr.getMeta(ySyncPluginKey);
121
+ if (change !== undefined) {
122
+ pluginState = Object.assign({}, pluginState);
123
+ for (const key in change) {
124
+ pluginState[key] = change[key];
125
+ }
126
+ }
127
+ pluginState.addToHistory = tr.getMeta('addToHistory') !== false;
128
+ // always set isChangeOrigin. If undefined, this is not change origin.
129
+ pluginState.isChangeOrigin = change !== undefined &&
130
+ !!change.isChangeOrigin;
131
+ pluginState.isUndoRedoOperation = change !== undefined &&
132
+ !!change.isChangeOrigin && !!change.isUndoRedoOperation;
133
+ if (binding.prosemirrorView !== null) {
134
+ if (change !== undefined &&
135
+ (change.snapshot != null || change.prevSnapshot != null)) {
136
+ // snapshot changed, rerender next
137
+ eventloop.timeout(0, () => {
138
+ if (binding.prosemirrorView == null) {
139
+ return;
140
+ }
141
+ if (change.restore == null) {
142
+ binding._renderSnapshot(change.snapshot, change.prevSnapshot, pluginState);
143
+ }
144
+ else {
145
+ binding._renderSnapshot(change.snapshot, change.snapshot, pluginState);
146
+ // reset to current prosemirror state
147
+ delete pluginState.restore;
148
+ delete pluginState.snapshot;
149
+ delete pluginState.prevSnapshot;
150
+ binding.mux(() => {
151
+ binding._prosemirrorChanged(binding.prosemirrorView.state.doc);
152
+ });
153
+ }
154
+ });
155
+ }
156
+ }
157
+ return pluginState;
158
+ },
159
+ },
160
+ view: (view) => {
161
+ binding.initView(view);
162
+ if (mapping == null) {
163
+ // force rerender to update the bindings mapping
164
+ binding._forceRerender();
165
+ }
166
+ onFirstRender();
167
+ return {
168
+ update: () => {
169
+ const pluginState = plugin.getState(view.state);
170
+ if (pluginState.snapshot == null && pluginState.prevSnapshot == null) {
171
+ if (
172
+ // If the content doesn't change initially, we don't render anything to Yjs
173
+ // If the content was cleared by a user action, we want to catch the change and
174
+ // represent it in Yjs
175
+ initialContentChanged ||
176
+ view.state.doc.content.findDiffStart(view.state.doc.type.createAndFill().content) !== null) {
177
+ initialContentChanged = true;
178
+ if (pluginState.addToHistory === false &&
179
+ !pluginState.isChangeOrigin) {
180
+ const yUndoPluginState = yUndoPluginKey.getState(view.state);
181
+ /**
182
+ * @type {Y.UndoManager}
183
+ */
184
+ const um = yUndoPluginState && yUndoPluginState.undoManager;
185
+ if (um) {
186
+ um.stopCapturing();
187
+ }
188
+ }
189
+ binding.mux(() => {
190
+ /** @type {Y.Doc} */ (pluginState.doc).transact((tr) => {
191
+ tr.meta.set('addToHistory', pluginState.addToHistory);
192
+ binding._prosemirrorChanged(view.state.doc);
193
+ }, ySyncPluginKey);
194
+ });
195
+ }
196
+ }
197
+ },
198
+ destroy: () => {
199
+ binding.destroy();
200
+ },
201
+ };
202
+ },
203
+ });
204
+ return plugin;
205
+ };
206
+ /**
207
+ * @param {import('prosemirror-state').Transaction} tr
208
+ * @param {ReturnType<typeof getRelativeSelection>} relSel
209
+ * @param {ProsemirrorBinding} binding
210
+ */
211
+ const restoreRelativeSelection = (tr, relSel, binding) => {
212
+ if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
213
+ if (relSel.type === 'all') {
214
+ tr.setSelection(new AllSelection(tr.doc));
215
+ }
216
+ else if (relSel.type === 'node') {
217
+ const anchor = relativePositionToAbsolutePosition(binding.doc, binding.type, relSel.anchor, binding.mapping);
218
+ tr.setSelection(NodeSelection.create(tr.doc, anchor));
219
+ }
220
+ else {
221
+ const anchor = relativePositionToAbsolutePosition(binding.doc, binding.type, relSel.anchor, binding.mapping);
222
+ const head = relativePositionToAbsolutePosition(binding.doc, binding.type, relSel.head, binding.mapping);
223
+ if (anchor !== null && head !== null) {
224
+ const sel = TextSelection.between(tr.doc.resolve(anchor), tr.doc.resolve(head));
225
+ tr.setSelection(sel);
226
+ }
227
+ }
228
+ }
229
+ };
230
+ /**
231
+ * @param {ProsemirrorBinding} pmbinding
232
+ * @param {import('prosemirror-state').EditorState} state
233
+ */
234
+ export const getRelativeSelection = (pmbinding, state) => ({
235
+ type: /** @type {any} */ (state.selection).jsonID,
236
+ anchor: absolutePositionToRelativePosition(state.selection.anchor, pmbinding.type, pmbinding.mapping),
237
+ head: absolutePositionToRelativePosition(state.selection.head, pmbinding.type, pmbinding.mapping),
238
+ });
239
+ /**
240
+ * Binding for prosemirror.
241
+ *
242
+ * @protected
243
+ */
244
+ export class ProsemirrorBinding {
245
+ /**
246
+ * @param {Y.XmlFragment} yXmlFragment The bind source
247
+ * @param {ProsemirrorMapping} mapping
248
+ */
249
+ constructor(yXmlFragment, mapping = new Map()) {
250
+ this.type = yXmlFragment;
251
+ /**
252
+ * this will be set once the view is created
253
+ * @type {any}
254
+ */
255
+ this.prosemirrorView = null;
256
+ this.mux = createMutex();
257
+ this.mapping = mapping;
258
+ /**
259
+ * Is overlapping mark - i.e. mark does not exclude itself.
260
+ *
261
+ * @type {Map<import('prosemirror-model').MarkType, boolean>}
262
+ */
263
+ this.isOMark = new Map();
264
+ this._observeFunction = this._typeChanged.bind(this);
265
+ /**
266
+ * @type {Y.Doc}
267
+ */
268
+ // @ts-ignore
269
+ this.doc = yXmlFragment.doc;
270
+ /**
271
+ * current selection as relative positions in the Yjs model
272
+ */
273
+ this.beforeTransactionSelection = null;
274
+ this.beforeAllTransactions = () => {
275
+ if (this.beforeTransactionSelection === null && this.prosemirrorView != null) {
276
+ this.beforeTransactionSelection = getRelativeSelection(this, this.prosemirrorView.state);
277
+ }
278
+ };
279
+ this.afterAllTransactions = () => {
280
+ this.beforeTransactionSelection = null;
281
+ };
282
+ this._domSelectionInView = null;
283
+ }
284
+ /**
285
+ * Create a transaction for changing the prosemirror state.
286
+ *
287
+ * @returns
288
+ */
289
+ get _tr() {
290
+ return this.prosemirrorView.state.tr.setMeta('addToHistory', false);
291
+ }
292
+ _isLocalCursorInView() {
293
+ if (!this.prosemirrorView.hasFocus())
294
+ return false;
295
+ if (environment.isBrowser && this._domSelectionInView === null) {
296
+ // Calculate the domSelectionInView and clear by next tick after all events are finished
297
+ eventloop.timeout(0, () => {
298
+ this._domSelectionInView = null;
299
+ });
300
+ this._domSelectionInView = this._isDomSelectionInView();
301
+ }
302
+ return this._domSelectionInView;
303
+ }
304
+ _isDomSelectionInView() {
305
+ const selection = this.prosemirrorView._root.getSelection();
306
+ if (selection == null || selection.anchorNode == null)
307
+ return false;
308
+ const range = dom.doc.createRange(); // https://github.com/yjs/y-prosemirror/pull/193
309
+ range.setStart(selection.anchorNode, selection.anchorOffset);
310
+ range.setEnd(selection.focusNode, selection.focusOffset);
311
+ // This is a workaround for an edgecase where getBoundingClientRect will
312
+ // return zero values if the selection is collapsed at the start of a newline
313
+ // see reference here: https://stackoverflow.com/a/59780954
314
+ const rects = range.getClientRects();
315
+ if (rects.length === 0) {
316
+ // probably buggy newline behavior, explicitly select the node contents
317
+ if (range.startContainer && range.collapsed) {
318
+ range.selectNodeContents(range.startContainer);
319
+ }
320
+ }
321
+ const bounding = range.getBoundingClientRect();
322
+ const documentElement = dom.doc.documentElement;
323
+ return bounding.bottom >= 0 && bounding.right >= 0 &&
324
+ bounding.left <=
325
+ (globalThis.innerWidth || documentElement.clientWidth || 0) &&
326
+ bounding.top <= (globalThis.innerHeight || documentElement.clientHeight || 0);
327
+ }
328
+ /**
329
+ * @param {Y.Snapshot} snapshot
330
+ * @param {Y.Snapshot} prevSnapshot
331
+ */
332
+ renderSnapshot(snapshot, prevSnapshot) {
333
+ if (!prevSnapshot) {
334
+ prevSnapshot = Y.createSnapshot(Y.createDeleteSet(), new Map());
335
+ }
336
+ this.prosemirrorView.dispatch(this._tr.setMeta(ySyncPluginKey, { snapshot, prevSnapshot }));
337
+ }
338
+ unrenderSnapshot() {
339
+ this.mapping.clear();
340
+ this.mux(() => {
341
+ const fragmentContent = this.type.toArray().map((t) => createNodeFromYElement(
342
+ /** @type {Y.XmlElement} */ (t), this.prosemirrorView.state.schema, this)).filter((n) => n !== null);
343
+ // @ts-ignore
344
+ const tr = this._tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0));
345
+ tr.setMeta(ySyncPluginKey, { snapshot: null, prevSnapshot: null });
346
+ this.prosemirrorView.dispatch(tr);
347
+ });
348
+ }
349
+ _forceRerender() {
350
+ this.mapping.clear();
351
+ this.mux(() => {
352
+ // If this is a forced rerender, this might neither happen as a pm change nor within a Yjs
353
+ // transaction. Then the "before selection" doesn't exist. In this case, we need to create a
354
+ // relative position before replacing content. Fixes #126
355
+ const sel = this.beforeTransactionSelection !== null
356
+ ? null
357
+ : this.prosemirrorView.state.selection;
358
+ const fragmentContent = this.type.toArray().map((t) => createNodeFromYElement(
359
+ /** @type {Y.XmlElement} */ (t), this.prosemirrorView.state.schema, this)).filter((n) => n !== null);
360
+ // @ts-ignore
361
+ const tr = this._tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0));
362
+ if (sel) {
363
+ /**
364
+ * If the Prosemirror document we just created from this.type is
365
+ * smaller than the previous document, the selection might be
366
+ * out of bound, which would make Prosemirror throw an error.
367
+ */
368
+ const clampedAnchor = math.min(math.max(sel.anchor, 0), tr.doc.content.size);
369
+ const clampedHead = math.min(math.max(sel.head, 0), tr.doc.content.size);
370
+ tr.setSelection(TextSelection.create(tr.doc, clampedAnchor, clampedHead));
371
+ }
372
+ this.prosemirrorView.dispatch(tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, binding: this }));
373
+ });
374
+ }
375
+ /**
376
+ * @param {Y.Snapshot|Uint8Array} snapshot
377
+ * @param {Y.Snapshot|Uint8Array} prevSnapshot
378
+ * @param {Object} pluginState
379
+ */
380
+ _renderSnapshot(snapshot, prevSnapshot, pluginState) {
381
+ /**
382
+ * The document that contains the full history of this document.
383
+ * @type {Y.Doc}
384
+ */
385
+ let historyDoc = this.doc;
386
+ let historyType = this.type;
387
+ if (!snapshot) {
388
+ snapshot = Y.snapshot(this.doc);
389
+ }
390
+ if (snapshot instanceof Uint8Array || prevSnapshot instanceof Uint8Array) {
391
+ if (!(snapshot instanceof Uint8Array) ||
392
+ !(prevSnapshot instanceof Uint8Array)) {
393
+ // expected both snapshots to be v2 updates
394
+ error.unexpectedCase();
395
+ }
396
+ historyDoc = new Y.Doc({ gc: false });
397
+ Y.applyUpdateV2(historyDoc, prevSnapshot);
398
+ prevSnapshot = Y.snapshot(historyDoc);
399
+ Y.applyUpdateV2(historyDoc, snapshot);
400
+ snapshot = Y.snapshot(historyDoc);
401
+ if (historyType._item === null) {
402
+ /**
403
+ * If is a root type, we need to find the root key in the initial document
404
+ * and use it to get the history type.
405
+ */
406
+ const rootKey = Array.from(this.doc.share.keys()).find((key) => this.doc.share.get(key) === this.type);
407
+ historyType = historyDoc.getXmlFragment(rootKey);
408
+ }
409
+ else {
410
+ /**
411
+ * If it is a sub type, we use the item id to find the history type.
412
+ */
413
+ const historyStructs = historyDoc.store.clients.get(historyType._item.id.client) ?? [];
414
+ const itemIndex = Y.findIndexSS(historyStructs, historyType._item.id.clock);
415
+ const item = /** @type {Y.Item} */ (historyStructs[itemIndex]);
416
+ const content = /** @type {Y.ContentType} */ (item.content);
417
+ historyType = /** @type {Y.XmlFragment} */ (content.type);
418
+ }
419
+ }
420
+ // clear mapping because we are going to rerender
421
+ this.mapping.clear();
422
+ this.mux(() => {
423
+ historyDoc.transact((transaction) => {
424
+ // before rendering, we are going to sanitize ops and split deleted ops
425
+ // if they were deleted by seperate users.
426
+ /**
427
+ * @type {Y.PermanentUserData}
428
+ */
429
+ const pud = pluginState.permanentUserData;
430
+ if (pud) {
431
+ pud.dss.forEach((ds) => {
432
+ Y.iterateDeletedStructs(transaction, ds, (_item) => { });
433
+ });
434
+ }
435
+ /**
436
+ * @param {'removed'|'added'} type
437
+ * @param {Y.ID} id
438
+ */
439
+ const computeYChange = (type, id) => {
440
+ const user = type === 'added'
441
+ ? pud.getUserByClientId(id.client)
442
+ : pud.getUserByDeletedId(id);
443
+ return {
444
+ user,
445
+ type,
446
+ color: getUserColor(pluginState.colorMapping, pluginState.colors, user),
447
+ };
448
+ };
449
+ // Create document fragment and render
450
+ const fragmentContent = Y.typeListToArraySnapshot(historyType, new Y.Snapshot(prevSnapshot.ds, snapshot.sv)).map((t) => {
451
+ if (!t._item.deleted || isVisible(t._item, snapshot) ||
452
+ isVisible(t._item, prevSnapshot)) {
453
+ return createNodeFromYElement(t, this.prosemirrorView.state.schema, { mapping: new Map(), isOMark: new Map() }, snapshot, prevSnapshot, computeYChange);
454
+ }
455
+ else {
456
+ // No need to render elements that are not visible by either snapshot.
457
+ // If a client adds and deletes content in the same snapshot the element is not visible by either snapshot.
458
+ return null;
459
+ }
460
+ }).filter((n) => n !== null);
461
+ // @ts-ignore
462
+ const tr = this._tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0));
463
+ this.prosemirrorView.dispatch(tr.setMeta(ySyncPluginKey, { isChangeOrigin: true }));
464
+ }, ySyncPluginKey);
465
+ });
466
+ }
467
+ /**
468
+ * @param {Array<Y.YEvent<any>>} events
469
+ * @param {Y.Transaction} transaction
470
+ */
471
+ _typeChanged(events, transaction) {
472
+ if (this.prosemirrorView == null)
473
+ return;
474
+ const syncState = ySyncPluginKey.getState(this.prosemirrorView.state);
475
+ if (events.length === 0 || syncState.snapshot != null ||
476
+ syncState.prevSnapshot != null) {
477
+ // drop out if snapshot is active
478
+ this.renderSnapshot(syncState.snapshot, syncState.prevSnapshot);
479
+ return;
480
+ }
481
+ this.mux(() => {
482
+ /**
483
+ * @param {any} _
484
+ * @param {Y.AbstractType<any>} type
485
+ */
486
+ const delType = (_, type) => this.mapping.delete(type);
487
+ Y.iterateDeletedStructs(transaction, transaction.deleteSet, (struct) => {
488
+ if (struct.constructor === Y.Item) {
489
+ const type =
490
+ /** @type {Y.ContentType} */ ( /** @type {Y.Item} */(struct)
491
+ .content).type;
492
+ type && this.mapping.delete(type);
493
+ }
494
+ });
495
+ transaction.changed.forEach(delType);
496
+ transaction.changedParentTypes.forEach(delType);
497
+ const fragmentContent = this.type.toArray().map((t) => createNodeIfNotExists(
498
+ /** @type {Y.XmlElement | Y.XmlHook} */ (t), this.prosemirrorView.state.schema, this)).filter((n) => n !== null);
499
+ // @ts-ignore
500
+ let tr = this._tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0));
501
+ restoreRelativeSelection(tr, this.beforeTransactionSelection, this);
502
+ tr = tr.setMeta(ySyncPluginKey, {
503
+ isChangeOrigin: true,
504
+ isUndoRedoOperation: transaction.origin instanceof Y.UndoManager,
505
+ });
506
+ if (this.beforeTransactionSelection !== null && this._isLocalCursorInView()) {
507
+ tr.scrollIntoView();
508
+ }
509
+ this.prosemirrorView.dispatch(tr);
510
+ });
511
+ }
512
+ /**
513
+ * @param {import('prosemirror-model').Node} doc
514
+ */
515
+ _prosemirrorChanged(doc) {
516
+ this.doc.transact(() => {
517
+ updateYFragment(this.doc, this.type, doc, this);
518
+ this.beforeTransactionSelection = getRelativeSelection(this, this.prosemirrorView.state);
519
+ }, ySyncPluginKey);
520
+ }
521
+ /**
522
+ * View is ready to listen to changes. Register observers.
523
+ * @param {any} prosemirrorView
524
+ */
525
+ initView(prosemirrorView) {
526
+ if (this.prosemirrorView != null)
527
+ this.destroy();
528
+ this.prosemirrorView = prosemirrorView;
529
+ this.doc.on('beforeAllTransactions', this.beforeAllTransactions);
530
+ this.doc.on('afterAllTransactions', this.afterAllTransactions);
531
+ this.type.observeDeep(this._observeFunction);
532
+ }
533
+ destroy() {
534
+ if (this.prosemirrorView == null)
535
+ return;
536
+ this.prosemirrorView = null;
537
+ this.type.unobserveDeep(this._observeFunction);
538
+ this.doc.off('beforeAllTransactions', this.beforeAllTransactions);
539
+ this.doc.off('afterAllTransactions', this.afterAllTransactions);
540
+ }
541
+ }
542
+ /**
543
+ * @private
544
+ * @param {Y.XmlElement | Y.XmlHook} el
545
+ * @param {PModel.Schema} schema
546
+ * @param {BindingMetadata} meta
547
+ * @param {Y.Snapshot} [snapshot]
548
+ * @param {Y.Snapshot} [prevSnapshot]
549
+ * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
550
+ * @return {PModel.Node | null}
551
+ */
552
+ const createNodeIfNotExists = (el, schema, meta, snapshot, prevSnapshot, computeYChange) => {
553
+ const node = /** @type {PModel.Node} */ (meta.mapping.get(el));
554
+ if (node === undefined) {
555
+ if (el instanceof Y.XmlElement) {
556
+ return createNodeFromYElement(el, schema, meta, snapshot, prevSnapshot, computeYChange);
557
+ }
558
+ else {
559
+ throw error.methodUnimplemented(); // we are currently not handling hooks
560
+ }
561
+ }
562
+ return node;
563
+ };
564
+ /**
565
+ * @private
566
+ * @param {Y.XmlElement} el
567
+ * @param {any} schema
568
+ * @param {BindingMetadata} meta
569
+ * @param {Y.Snapshot} [snapshot]
570
+ * @param {Y.Snapshot} [prevSnapshot]
571
+ * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
572
+ * @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
573
+ */
574
+ export const createNodeFromYElement = (el, schema, meta, snapshot, prevSnapshot, computeYChange) => {
575
+ const children = [];
576
+ /**
577
+ * @param {Y.XmlElement | Y.XmlText} type
578
+ */
579
+ const createChildren = (type) => {
580
+ if (type instanceof Y.XmlElement) {
581
+ const n = createNodeIfNotExists(type, schema, meta, snapshot, prevSnapshot, computeYChange);
582
+ if (n !== null) {
583
+ children.push(n);
584
+ }
585
+ }
586
+ else {
587
+ // If the next ytext exists and was created by us, move the content to the current ytext.
588
+ // This is a fix for #160 -- duplication of characters when two Y.Text exist next to each
589
+ // other.
590
+ const nextytext = /** @type {Y.ContentType} */ (type._item.right?.content)
591
+ ?.type;
592
+ if (nextytext instanceof Y.Text && !nextytext._item.deleted &&
593
+ nextytext._item.id.client === nextytext.doc.clientID) {
594
+ type.applyDelta([
595
+ { retain: type.length },
596
+ ...nextytext.toDelta(),
597
+ ]);
598
+ nextytext.doc.transact((tr) => {
599
+ nextytext._item.delete(tr);
600
+ });
601
+ }
602
+ // now create the prosemirror text nodes
603
+ const ns = createTextNodesFromYText(type, schema, meta, snapshot, prevSnapshot, computeYChange);
604
+ if (ns !== null) {
605
+ ns.forEach((textchild) => {
606
+ if (textchild !== null) {
607
+ children.push(textchild);
608
+ }
609
+ });
610
+ }
611
+ }
612
+ };
613
+ if (snapshot === undefined || prevSnapshot === undefined) {
614
+ el.toArray().forEach(createChildren);
615
+ }
616
+ else {
617
+ Y.typeListToArraySnapshot(el, new Y.Snapshot(prevSnapshot.ds, snapshot.sv))
618
+ .forEach(createChildren);
619
+ }
620
+ try {
621
+ const attrs = el.getAttributes(snapshot);
622
+ if (snapshot !== undefined) {
623
+ if (!isVisible(/** @type {Y.Item} */ (el._item), snapshot)) {
624
+ attrs.ychange = computeYChange
625
+ ? computeYChange('removed', /** @type {Y.Item} */ (el._item).id)
626
+ : { type: 'removed' };
627
+ }
628
+ else if (!isVisible(/** @type {Y.Item} */ (el._item), prevSnapshot)) {
629
+ attrs.ychange = computeYChange
630
+ ? computeYChange('added', /** @type {Y.Item} */ (el._item).id)
631
+ : { type: 'added' };
632
+ }
633
+ }
634
+ const node = schema.node(el.nodeName, attrs, children);
635
+ meta.mapping.set(el, node);
636
+ return node;
637
+ }
638
+ catch (e) {
639
+ // an error occured while creating the node. This is probably a result of a concurrent action.
640
+ /** @type {Y.Doc} */ (el.doc).transact((transaction) => {
641
+ /** @type {Y.Item} */ (el._item).delete(transaction);
642
+ }, ySyncPluginKey);
643
+ meta.mapping.delete(el);
644
+ return null;
645
+ }
646
+ };
647
+ /**
648
+ * @private
649
+ * @param {Y.XmlText} text
650
+ * @param {import('prosemirror-model').Schema} schema
651
+ * @param {BindingMetadata} _meta
652
+ * @param {Y.Snapshot} [snapshot]
653
+ * @param {Y.Snapshot} [prevSnapshot]
654
+ * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
655
+ * @return {Array<PModel.Node>|null}
656
+ */
657
+ const createTextNodesFromYText = (text, schema, _meta, snapshot, prevSnapshot, computeYChange) => {
658
+ const nodes = [];
659
+ const deltas = text.toDelta(snapshot, prevSnapshot, computeYChange);
660
+ try {
661
+ for (let i = 0; i < deltas.length; i++) {
662
+ const delta = deltas[i];
663
+ nodes.push(schema.text(delta.insert, attributesToMarks(delta.attributes, schema)));
664
+ }
665
+ }
666
+ catch (e) {
667
+ // an error occured while creating the node. This is probably a result of a concurrent action.
668
+ /** @type {Y.Doc} */ (text.doc).transact((transaction) => {
669
+ /** @type {Y.Item} */ (text._item).delete(transaction);
670
+ }, ySyncPluginKey);
671
+ return null;
672
+ }
673
+ // @ts-ignore
674
+ return nodes;
675
+ };
676
+ /**
677
+ * @private
678
+ * @param {Array<any>} nodes prosemirror node
679
+ * @param {BindingMetadata} meta
680
+ * @return {Y.XmlText}
681
+ */
682
+ const createTypeFromTextNodes = (nodes, meta) => {
683
+ const type = new Y.XmlText();
684
+ const delta = nodes.map((node) => ({
685
+ // @ts-ignore
686
+ insert: node.text,
687
+ attributes: marksToAttributes(node.marks, meta),
688
+ }));
689
+ type.applyDelta(delta);
690
+ meta.mapping.set(type, nodes);
691
+ return type;
692
+ };
693
+ /**
694
+ * @private
695
+ * @param {any} node prosemirror node
696
+ * @param {BindingMetadata} meta
697
+ * @return {Y.XmlElement}
698
+ */
699
+ const createTypeFromElementNode = (node, meta) => {
700
+ const type = new Y.XmlElement(node.type.name);
701
+ for (const key in node.attrs) {
702
+ const val = node.attrs[key];
703
+ if (val !== null && key !== 'ychange') {
704
+ type.setAttribute(key, val);
705
+ }
706
+ }
707
+ type.insert(0, normalizePNodeContent(node).map((n) => createTypeFromTextOrElementNode(n, meta)));
708
+ meta.mapping.set(type, node);
709
+ return type;
710
+ };
711
+ /**
712
+ * @private
713
+ * @param {PModel.Node|Array<PModel.Node>} node prosemirror text node
714
+ * @param {BindingMetadata} meta
715
+ * @return {Y.XmlElement|Y.XmlText}
716
+ */
717
+ const createTypeFromTextOrElementNode = (node, meta) => node instanceof Array
718
+ ? createTypeFromTextNodes(node, meta)
719
+ : createTypeFromElementNode(node, meta);
720
+ /**
721
+ * @param {any} val
722
+ */
723
+ const isObject = (val) => typeof val === 'object' && val !== null;
724
+ /**
725
+ * @param {any} pattrs
726
+ * @param {any} yattrs
727
+ */
728
+ const equalAttrs = (pattrs, yattrs) => {
729
+ const keys = Object.keys(pattrs).filter((key) => pattrs[key] !== null);
730
+ let eq = keys.length ===
731
+ (yattrs == null
732
+ ? 0
733
+ : Object.keys(yattrs).filter((key) => yattrs[key] !== null).length);
734
+ for (let i = 0; i < keys.length && eq; i++) {
735
+ const key = keys[i];
736
+ const l = pattrs[key];
737
+ const r = yattrs[key];
738
+ eq = key === 'ychange' || l === r ||
739
+ (isObject(l) && isObject(r) && equalAttrs(l, r));
740
+ }
741
+ return eq;
742
+ };
743
+ /**
744
+ * @typedef {Array<Array<PModel.Node>|PModel.Node>} NormalizedPNodeContent
745
+ */
746
+ /**
747
+ * @param {any} pnode
748
+ * @return {NormalizedPNodeContent}
749
+ */
750
+ const normalizePNodeContent = (pnode) => {
751
+ const c = pnode.content.content;
752
+ const res = [];
753
+ for (let i = 0; i < c.length; i++) {
754
+ const n = c[i];
755
+ if (n.isText) {
756
+ const textNodes = [];
757
+ for (let tnode = c[i]; i < c.length && tnode.isText; tnode = c[++i]) {
758
+ textNodes.push(tnode);
759
+ }
760
+ i--;
761
+ res.push(textNodes);
762
+ }
763
+ else {
764
+ res.push(n);
765
+ }
766
+ }
767
+ return res;
768
+ };
769
+ /**
770
+ * @param {Y.XmlText} ytext
771
+ * @param {Array<any>} ptexts
772
+ */
773
+ const equalYTextPText = (ytext, ptexts) => {
774
+ const delta = ytext.toDelta();
775
+ return delta.length === ptexts.length &&
776
+ delta.every(/** @type {(d:any,i:number) => boolean} */ (d, i) => d.insert === /** @type {any} */ (ptexts[i]).text &&
777
+ object.keys(d.attributes || {}).length === ptexts[i].marks.length &&
778
+ object.every(d.attributes, (attr, yattrname) => {
779
+ const markname = yattr2markname(yattrname);
780
+ const pmarks = ptexts[i].marks;
781
+ return equalAttrs(attr, pmarks.find(/** @param {any} mark */ (mark) => mark.type.name === markname)?.attrs);
782
+ }));
783
+ };
784
+ /**
785
+ * @param {Y.XmlElement|Y.XmlText|Y.XmlHook} ytype
786
+ * @param {any|Array<any>} pnode
787
+ */
788
+ const equalYTypePNode = (ytype, pnode) => {
789
+ if (ytype instanceof Y.XmlElement && !(pnode instanceof Array) &&
790
+ matchNodeName(ytype, pnode)) {
791
+ const normalizedContent = normalizePNodeContent(pnode);
792
+ return ytype._length === normalizedContent.length &&
793
+ equalAttrs(ytype.getAttributes(), pnode.attrs) &&
794
+ ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, normalizedContent[i]));
795
+ }
796
+ return ytype instanceof Y.XmlText && pnode instanceof Array &&
797
+ equalYTextPText(ytype, pnode);
798
+ };
799
+ /**
800
+ * @param {PModel.Node | Array<PModel.Node> | undefined} mapped
801
+ * @param {PModel.Node | Array<PModel.Node>} pcontent
802
+ */
803
+ const mappedIdentity = (mapped, pcontent) => mapped === pcontent ||
804
+ (mapped instanceof Array && pcontent instanceof Array &&
805
+ mapped.length === pcontent.length &&
806
+ mapped.every((a, i) => pcontent[i] === a));
807
+ /**
808
+ * @param {Y.XmlElement} ytype
809
+ * @param {PModel.Node} pnode
810
+ * @param {BindingMetadata} meta
811
+ * @return {{ foundMappedChild: boolean, equalityFactor: number }}
812
+ */
813
+ const computeChildEqualityFactor = (ytype, pnode, meta) => {
814
+ const yChildren = ytype.toArray();
815
+ const pChildren = normalizePNodeContent(pnode);
816
+ const pChildCnt = pChildren.length;
817
+ const yChildCnt = yChildren.length;
818
+ const minCnt = math.min(yChildCnt, pChildCnt);
819
+ let left = 0;
820
+ let right = 0;
821
+ let foundMappedChild = false;
822
+ for (; left < minCnt; left++) {
823
+ const leftY = yChildren[left];
824
+ const leftP = pChildren[left];
825
+ if (mappedIdentity(meta.mapping.get(leftY), leftP)) {
826
+ foundMappedChild = true; // definite (good) match!
827
+ }
828
+ else if (!equalYTypePNode(leftY, leftP)) {
829
+ break;
830
+ }
831
+ }
832
+ for (; left + right < minCnt; right++) {
833
+ const rightY = yChildren[yChildCnt - right - 1];
834
+ const rightP = pChildren[pChildCnt - right - 1];
835
+ if (mappedIdentity(meta.mapping.get(rightY), rightP)) {
836
+ foundMappedChild = true;
837
+ }
838
+ else if (!equalYTypePNode(rightY, rightP)) {
839
+ break;
840
+ }
841
+ }
842
+ return {
843
+ equalityFactor: left + right,
844
+ foundMappedChild,
845
+ };
846
+ };
847
+ /**
848
+ * @param {Y.Text} ytext
849
+ */
850
+ const ytextTrans = (ytext) => {
851
+ let str = '';
852
+ /**
853
+ * @type {Y.Item|null}
854
+ */
855
+ let n = ytext._start;
856
+ const nAttrs = {};
857
+ while (n !== null) {
858
+ if (!n.deleted) {
859
+ if (n.countable && n.content instanceof Y.ContentString) {
860
+ str += n.content.str;
861
+ }
862
+ else if (n.content instanceof Y.ContentFormat) {
863
+ nAttrs[n.content.key] = null;
864
+ }
865
+ }
866
+ n = n.right;
867
+ }
868
+ return {
869
+ str,
870
+ nAttrs,
871
+ };
872
+ };
873
+ /**
874
+ * @todo test this more
875
+ *
876
+ * @param {Y.Text} ytext
877
+ * @param {Array<any>} ptexts
878
+ * @param {BindingMetadata} meta
879
+ */
880
+ const updateYText = (ytext, ptexts, meta) => {
881
+ meta.mapping.set(ytext, ptexts);
882
+ const { nAttrs, str } = ytextTrans(ytext);
883
+ const content = ptexts.map((p) => ({
884
+ insert: /** @type {any} */ (p).text,
885
+ attributes: Object.assign({}, nAttrs, marksToAttributes(p.marks, meta)),
886
+ }));
887
+ const { insert, remove, index } = simpleDiff(str, content.map((c) => c.insert).join(''));
888
+ ytext.delete(index, remove);
889
+ ytext.insert(index, insert);
890
+ ytext.applyDelta(content.map((c) => ({ retain: c.insert.length, attributes: c.attributes })));
891
+ };
892
+ const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/;
893
+ /**
894
+ * @param {string} attrName
895
+ */
896
+ export const yattr2markname = (attrName) => hashedMarkNameRegex.exec(attrName)?.[1] ?? attrName;
897
+ /**
898
+ * @todo move this to markstoattributes
899
+ *
900
+ * @param {Object<string, any>} attrs
901
+ * @param {import('prosemirror-model').Schema} schema
902
+ */
903
+ export const attributesToMarks = (attrs, schema) => {
904
+ /**
905
+ * @type {Array<import('prosemirror-model').Mark>}
906
+ */
907
+ const marks = [];
908
+ for (const markName in attrs) {
909
+ // remove hashes if necessary
910
+ marks.push(schema.mark(yattr2markname(markName), attrs[markName]));
911
+ }
912
+ return marks;
913
+ };
914
+ /**
915
+ * @param {Array<import('prosemirror-model').Mark>} marks
916
+ * @param {BindingMetadata} meta
917
+ */
918
+ const marksToAttributes = (marks, meta) => {
919
+ const pattrs = {};
920
+ marks.forEach((mark) => {
921
+ if (mark.type.name !== 'ychange') {
922
+ const isOverlapping = map.setIfUndefined(meta.isOMark, mark.type, () => !mark.type.excludes(mark.type));
923
+ pattrs[isOverlapping
924
+ ? `${mark.type.name}--${utils.hashOfJSON(mark.toJSON())}`
925
+ : mark.type.name] = mark.attrs;
926
+ }
927
+ });
928
+ return pattrs;
929
+ };
930
+ /**
931
+ * Update a yDom node by syncing the current content of the prosemirror node.
932
+ *
933
+ * This is a y-prosemirror internal feature that you can use at your own risk.
934
+ *
935
+ * @private
936
+ * @unstable
937
+ *
938
+ * @param {{transact: Function}} y
939
+ * @param {Y.XmlFragment} yDomFragment
940
+ * @param {any} pNode
941
+ * @param {BindingMetadata} meta
942
+ */
943
+ export const updateYFragment = (y, yDomFragment, pNode, meta) => {
944
+ if (yDomFragment instanceof Y.XmlElement &&
945
+ yDomFragment.nodeName !== pNode.type.name) {
946
+ throw new Error('node name mismatch!');
947
+ }
948
+ meta.mapping.set(yDomFragment, pNode);
949
+ // update attributes
950
+ if (yDomFragment instanceof Y.XmlElement) {
951
+ const yDomAttrs = yDomFragment.getAttributes();
952
+ const pAttrs = pNode.attrs;
953
+ for (const key in pAttrs) {
954
+ if (pAttrs[key] !== null) {
955
+ if (yDomAttrs[key] !== pAttrs[key] && key !== 'ychange') {
956
+ yDomFragment.setAttribute(key, pAttrs[key]);
957
+ }
958
+ }
959
+ else {
960
+ yDomFragment.removeAttribute(key);
961
+ }
962
+ }
963
+ // remove all keys that are no longer in pAttrs
964
+ for (const key in yDomAttrs) {
965
+ if (pAttrs[key] === undefined) {
966
+ yDomFragment.removeAttribute(key);
967
+ }
968
+ }
969
+ }
970
+ // update children
971
+ const pChildren = normalizePNodeContent(pNode);
972
+ const pChildCnt = pChildren.length;
973
+ const yChildren = yDomFragment.toArray();
974
+ const yChildCnt = yChildren.length;
975
+ const minCnt = math.min(pChildCnt, yChildCnt);
976
+ let left = 0;
977
+ let right = 0;
978
+ // find number of matching elements from left
979
+ for (; left < minCnt; left++) {
980
+ const leftY = yChildren[left];
981
+ const leftP = pChildren[left];
982
+ if (!mappedIdentity(meta.mapping.get(leftY), leftP)) {
983
+ if (equalYTypePNode(leftY, leftP)) {
984
+ // update mapping
985
+ meta.mapping.set(leftY, leftP);
986
+ }
987
+ else {
988
+ break;
989
+ }
990
+ }
991
+ }
992
+ // find number of matching elements from right
993
+ for (; right + left < minCnt; right++) {
994
+ const rightY = yChildren[yChildCnt - right - 1];
995
+ const rightP = pChildren[pChildCnt - right - 1];
996
+ if (!mappedIdentity(meta.mapping.get(rightY), rightP)) {
997
+ if (equalYTypePNode(rightY, rightP)) {
998
+ // update mapping
999
+ meta.mapping.set(rightY, rightP);
1000
+ }
1001
+ else {
1002
+ break;
1003
+ }
1004
+ }
1005
+ }
1006
+ y.transact(() => {
1007
+ // try to compare and update
1008
+ while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
1009
+ const leftY = yChildren[left];
1010
+ const leftP = pChildren[left];
1011
+ const rightY = yChildren[yChildCnt - right - 1];
1012
+ const rightP = pChildren[pChildCnt - right - 1];
1013
+ if (leftY instanceof Y.XmlText && leftP instanceof Array) {
1014
+ if (!equalYTextPText(leftY, leftP)) {
1015
+ updateYText(leftY, leftP, meta);
1016
+ }
1017
+ left += 1;
1018
+ }
1019
+ else {
1020
+ let updateLeft = leftY instanceof Y.XmlElement &&
1021
+ matchNodeName(leftY, leftP);
1022
+ let updateRight = rightY instanceof Y.XmlElement &&
1023
+ matchNodeName(rightY, rightP);
1024
+ if (updateLeft && updateRight) {
1025
+ // decide which which element to update
1026
+ const equalityLeft = computeChildEqualityFactor(
1027
+ /** @type {Y.XmlElement} */ (leftY),
1028
+ /** @type {PModel.Node} */ (leftP), meta);
1029
+ const equalityRight = computeChildEqualityFactor(
1030
+ /** @type {Y.XmlElement} */ (rightY),
1031
+ /** @type {PModel.Node} */ (rightP), meta);
1032
+ if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) {
1033
+ updateRight = false;
1034
+ }
1035
+ else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) {
1036
+ updateLeft = false;
1037
+ }
1038
+ else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) {
1039
+ updateLeft = false;
1040
+ }
1041
+ else {
1042
+ updateRight = false;
1043
+ }
1044
+ }
1045
+ if (updateLeft) {
1046
+ updateYFragment(y,
1047
+ /** @type {Y.XmlFragment} */ (leftY),
1048
+ /** @type {PModel.Node} */ (leftP), meta);
1049
+ left += 1;
1050
+ }
1051
+ else if (updateRight) {
1052
+ updateYFragment(y,
1053
+ /** @type {Y.XmlFragment} */ (rightY),
1054
+ /** @type {PModel.Node} */ (rightP), meta);
1055
+ right += 1;
1056
+ }
1057
+ else {
1058
+ meta.mapping.delete(yDomFragment.get(left));
1059
+ yDomFragment.delete(left, 1);
1060
+ yDomFragment.insert(left, [
1061
+ createTypeFromTextOrElementNode(leftP, meta),
1062
+ ]);
1063
+ left += 1;
1064
+ }
1065
+ }
1066
+ }
1067
+ const yDelLen = yChildCnt - left - right;
1068
+ if (yChildCnt === 1 && pChildCnt === 0 && yChildren[0] instanceof Y.XmlText) {
1069
+ meta.mapping.delete(yChildren[0]);
1070
+ // Edge case handling https://github.com/yjs/y-prosemirror/issues/108
1071
+ // Only delete the content of the Y.Text to retain remote changes on the same Y.Text object
1072
+ yChildren[0].delete(0, yChildren[0].length);
1073
+ }
1074
+ else if (yDelLen > 0) {
1075
+ yDomFragment.slice(left, left + yDelLen).forEach((type) => meta.mapping.delete(type));
1076
+ yDomFragment.delete(left, yDelLen);
1077
+ }
1078
+ if (left + right < pChildCnt) {
1079
+ const ins = [];
1080
+ for (let i = left; i < pChildCnt - right; i++) {
1081
+ ins.push(createTypeFromTextOrElementNode(pChildren[i], meta));
1082
+ }
1083
+ yDomFragment.insert(left, ins);
1084
+ }
1085
+ }, ySyncPluginKey);
1086
+ };
1087
+ /**
1088
+ * @function
1089
+ * @param {Y.XmlElement} yElement
1090
+ * @param {any} pNode Prosemirror Node
1091
+ */
1092
+ const matchNodeName = (yElement, pNode) => !(pNode instanceof Array) && yElement.nodeName === pNode.type.name;
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@kerebron/extension-yjs",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "module": "./esm/extension-yjs/src/ExtensionYjs.js",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./esm/extension-yjs/src/ExtensionYjs.js"
9
+ },
10
+ "./userColors": {
11
+ "import": "./esm/extension-yjs/src/userColors.js"
9
12
  }
10
13
  },
11
14
  "scripts": {},