@nerd-bible/wordgard 0.0.0-beta3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (C) 2026 by Marijn Haverbeke <marijn@haverbeke.berlin>.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Wordgard
2
+
3
+ [ [**WEBSITE**](https://wordgard.net/) | [**DOCS**](https://wordgard.net/docs/) | [**ISSUES**](https://code.haverbeke.berlin/wordgard/wordgard/issues) | [**FORUM**](https://discuss.wordgard.net/) ]
4
+
5
+ This is the [Wordgard](https://wordgard.net) package. It implements a
6
+ rich text editor framework for the browser.
7
+
8
+ ```bash
9
+ npm i wordgard
10
+ ```
11
+
12
+ ```javascript
13
+ import {Wordgard, menuBar} from "wordgard/editor"
14
+ import {fullSchema} from "wordgard/schema"
15
+ import {history} from "wordgard/history"
16
+
17
+ const myEditor = Wordgard.create({
18
+ parent: document.body,
19
+ doc: `<h2>Hello World</h2>`,
20
+ config: [fullSchema(), history(), menuBar()]
21
+ })
22
+ ```
23
+
24
+ This project is licensed under an MIT license.
@@ -0,0 +1,105 @@
1
+ import { GardState, Correction, Transaction } from 'wordgard/state';
2
+ import { ChangeSet, Plot } from 'wordgard/doc';
3
+
4
+ /**
5
+ Create an instance of the collaborative editing plugin.
6
+ */
7
+ declare function collab(config?: collab.Config): GardState.Extension;
8
+ declare namespace collab {
9
+ /**
10
+ Options to {@link collab}.
11
+ */
12
+ type Config = {
13
+ /**
14
+ The starting document version. Defaults to 0.
15
+ */
16
+ startVersion?: number;
17
+ /**
18
+ This client's identifying {@link collab.getClientID ID}. Will be a
19
+ randomly generated string if not provided.
20
+ */
21
+ clientID?: string;
22
+ /**
23
+ A set of corrections to apply to transformed changes. Can be
24
+ used to enforce document shapes even in merged changes.
25
+ Requires the exact same set, in the same order, to be used on
26
+ the server.
27
+ */
28
+ corrections?: readonly Correction[];
29
+ /**
30
+ It is possible to share information other than document changes
31
+ through this extension. If you provide this option, your
32
+ function will be called on each transaction, and the effects it
33
+ returns will be sent to the server, much like changes are. Such
34
+ effects are automatically remapped when conflicting remote
35
+ changes come in.
36
+ */
37
+ sharedEffects?: (tr: Transaction) => readonly Transaction.Effect<any>[];
38
+ };
39
+ /**
40
+ An update is a set of changes and effects.
41
+ */
42
+ interface Update {
43
+ /**
44
+ The document version that this update starts from.
45
+ */
46
+ version: number;
47
+ /**
48
+ The {@link collab.Config.clientID ID} of the client who
49
+ created this update.
50
+ */
51
+ clientID: string;
52
+ /**
53
+ The changes made by this update.
54
+ */
55
+ changes: ChangeSet;
56
+ /**
57
+ The effects in this update. There'll only ever be effects here
58
+ when you configure your collab extension with a {@link
59
+ collab.Config.sharedEffects `sharedEffects`} option.
60
+ */
61
+ effects?: readonly Transaction.Effect<unknown>[];
62
+ }
63
+ /**
64
+ Create a transaction that represents a set of new updates received
65
+ from the authority. Applying this transaction moves the state
66
+ forward to, for remote changes, integrate them into our local
67
+ state, and for our own changes, drop them from the set of
68
+ unconfirmed local changes.
69
+ */
70
+ function receive(state: GardState, updates: readonly collab.Update[]): Transaction;
71
+ /**
72
+ If there are unconfirmed local changes that need to be sent to the
73
+ server,return them as an `Update` object.
74
+ */
75
+ function sendableUpdate(state: GardState): collab.Update | null;
76
+ /**
77
+ Returns true if there are unconfirmed changes for this editor.
78
+ */
79
+ function hasUnsentUpdate(state: GardState): boolean;
80
+ /**
81
+ Get the version up to which the collab plugin has synced with the
82
+ central authority.
83
+ */
84
+ function getSyncedVersion(state: GardState): number;
85
+ /**
86
+ Get this editor's collaborative editing client ID.
87
+ */
88
+ function getClientID(state: GardState): string;
89
+ /**
90
+ Transform an update that arrives on the server with an outdated
91
+ start version. Being able to do this requires tracking a
92
+ document history server-side. It is not necessary to do this,
93
+ but it helps a lot with “starvation” problems, where slower
94
+ clients or clients with a high ping can, in a busy document,
95
+ keep losing the race to submit their changes to other, faster
96
+ clients.
97
+ */
98
+ function transformUpdate(update: collab.Update, over: readonly {
99
+ doc: Plot.Doc;
100
+ changes: ChangeSet;
101
+ clientID: string;
102
+ }[], corrections?: readonly Correction[]): collab.Update | null;
103
+ }
104
+
105
+ export { collab };
package/dist/collab.js ADDED
@@ -0,0 +1,218 @@
1
+ import { GardState, Transaction, Correction } from 'wordgard/state';
2
+ import { ChangeSet } from 'wordgard/doc';
3
+
4
+ class LocalUpdate {
5
+ changes;
6
+ effects;
7
+ constructor(changes, effects) {
8
+ this.changes = changes;
9
+ this.effects = effects;
10
+ }
11
+ }
12
+ function addUpdate(to, changes, effects = []) {
13
+ if (changes.empty && !effects.length)
14
+ return to;
15
+ if (!to)
16
+ return new LocalUpdate(changes, effects);
17
+ return new LocalUpdate(to.changes.compose(changes), Transaction.Effect.mapEffects(to.effects, changes).concat(effects));
18
+ }
19
+ function mapOpenUpdate(update, doc, over, accum) {
20
+ let { a, b } = ChangeSet.transform(doc, over, update.changes);
21
+ return [new LocalUpdate(b, Transaction.Effect.mapEffects(update.effects, a)), accum.compose(a)];
22
+ }
23
+ class CollabState {
24
+ version;
25
+ syncedDoc;
26
+ nextUpdate;
27
+ openUpdate;
28
+ constructor(
29
+ version,
30
+ syncedDoc,
31
+ nextUpdate,
32
+ openUpdate) {
33
+ this.version = version;
34
+ this.syncedDoc = syncedDoc;
35
+ this.nextUpdate = nextUpdate;
36
+ this.openUpdate = openUpdate;
37
+ }
38
+ }
39
+ const collabConfig = /*@__PURE__*/GardState.Facet.define({
40
+ combine(configs) {
41
+ let combined = GardState.Facet.combineConfig(configs, {
42
+ startVersion: 0,
43
+ clientID: null,
44
+ sharedEffects: () => [],
45
+ corrections: []
46
+ }, {
47
+ generatedID: a => a
48
+ });
49
+ if (combined.clientID == null)
50
+ combined.clientID = (configs.length && configs[0].generatedID) || "";
51
+ return combined;
52
+ }
53
+ });
54
+ const collabReceive = /*@__PURE__*/Transaction.Effect.define({
55
+ map(state, changes) {
56
+ return changes.empty ? state
57
+ : new CollabState(state.version, state.syncedDoc, state.nextUpdate, addUpdate(state.openUpdate, changes));
58
+ }
59
+ });
60
+ const collabField = /*@__PURE__*/GardState.Field.define({
61
+ create(state) {
62
+ return new CollabState(state.facet(collabConfig).startVersion, state.doc, null, null);
63
+ },
64
+ update(collab, tr) {
65
+ for (let e of tr.effects)
66
+ if (e.is(collabReceive))
67
+ return e.value;
68
+ let { sharedEffects } = tr.startState.facet(collabConfig);
69
+ let effects = sharedEffects(tr);
70
+ if (effects.length || !tr.changes.empty)
71
+ return new CollabState(collab.version, collab.syncedDoc, collab.nextUpdate, addUpdate(collab.openUpdate, tr.changes, effects));
72
+ return collab;
73
+ }
74
+ });
75
+ function collab(config = {}) {
76
+ return [collabField, collabConfig.of({ generatedID: Math.floor(Math.random() * 1e9).toString(36), ...config })];
77
+ }
78
+ ;collab = /*@__PURE__*/(function (collab_1) {
79
+ function receive(state, updates) {
80
+ let { version, syncedDoc, nextUpdate, openUpdate } = state.field(collabField);
81
+ let { clientID, corrections } = state.facet(collabConfig);
82
+ let changes = ChangeSet.empty(state.doc.length);
83
+ let effects = [];
84
+ let haveRemote = false;
85
+ for (let update of updates) {
86
+ if (update.version != version)
87
+ throw new Error("Version mismatch in in received collab update");
88
+ if (update.clientID == clientID) {
89
+ if (!nextUpdate)
90
+ throw new Error("Received unknown update with our client ID");
91
+ syncedDoc = (openUpdate || haveRemote) ? nextUpdate.changes.apply(syncedDoc) : state.doc;
92
+ mismatch: if (!nextUpdate.changes.eq(update.changes)) {
93
+ if (haveRemote && nextUpdate && corrections.length) {
94
+ let correct = Correction.check(nextUpdate.changes, syncedDoc, corrections);
95
+ if (correct && nextUpdate.changes.compose(correct).eq(update.changes)) {
96
+ if (openUpdate)
97
+ [openUpdate, changes] = mapOpenUpdate(openUpdate, syncedDoc, correct, changes);
98
+ else
99
+ changes = changes.compose(correct);
100
+ syncedDoc = correct.apply(syncedDoc);
101
+ break mismatch;
102
+ }
103
+ }
104
+ throw new Error("Received update with our client ID doesn't match our own local update");
105
+ }
106
+ nextUpdate = null;
107
+ }
108
+ else {
109
+ let newChanges = update.changes, newEffects = update.effects || [];
110
+ let baseDoc = syncedDoc;
111
+ if (nextUpdate) {
112
+ let { a, b } = ChangeSet.transform(baseDoc, newChanges, nextUpdate.changes);
113
+ if (openUpdate)
114
+ baseDoc = nextUpdate.changes.apply(baseDoc);
115
+ nextUpdate = new LocalUpdate(b, Transaction.Effect.mapEffects(nextUpdate.effects, a));
116
+ newChanges = a;
117
+ newEffects = Transaction.Effect.mapEffects(newEffects, b);
118
+ }
119
+ if (openUpdate) {
120
+ let { a, b } = ChangeSet.transform(baseDoc, newChanges, openUpdate.changes);
121
+ openUpdate = new LocalUpdate(b, Transaction.Effect.mapEffects(openUpdate.effects, a));
122
+ newChanges = a;
123
+ newEffects = Transaction.Effect.mapEffects(newEffects, b);
124
+ }
125
+ changes = changes.compose(newChanges);
126
+ effects = Transaction.Effect.mapEffects(effects, newChanges).concat(newEffects);
127
+ syncedDoc = update.changes.apply(syncedDoc);
128
+ haveRemote = true;
129
+ }
130
+ version++;
131
+ }
132
+ if (haveRemote && corrections.length) {
133
+ let base = syncedDoc;
134
+ if (nextUpdate) {
135
+ base = nextUpdate.changes.apply(base);
136
+ let correct = Correction.check(nextUpdate.changes, base, corrections);
137
+ if (correct) {
138
+ nextUpdate = addUpdate(nextUpdate, correct);
139
+ if (openUpdate) {
140
+ [openUpdate, changes] = mapOpenUpdate(openUpdate, base, correct, changes);
141
+ base = correct.apply(base);
142
+ }
143
+ else {
144
+ changes = changes.compose(correct);
145
+ }
146
+ }
147
+ }
148
+ if (openUpdate) {
149
+ base = openUpdate.changes.apply(base);
150
+ let correct = Correction.check(openUpdate.changes, base, corrections);
151
+ if (correct) {
152
+ openUpdate = addUpdate(openUpdate, correct);
153
+ changes = changes.compose(correct);
154
+ }
155
+ }
156
+ }
157
+ return state.update({
158
+ changes,
159
+ effects: effects.concat(collabReceive.of(new CollabState(version, syncedDoc, nextUpdate, openUpdate))),
160
+ annotations: [Transaction.addToHistory.of(false), Transaction.remote.of(true)],
161
+ });
162
+ }
163
+ collab_1.receive = receive;
164
+ function sendableUpdate(state) {
165
+ let collab = state.field(collabField);
166
+ if (!collab.nextUpdate) {
167
+ if (!collab.openUpdate)
168
+ return null;
169
+ collab.nextUpdate = collab.openUpdate;
170
+ collab.openUpdate = null;
171
+ }
172
+ return {
173
+ version: collab.version,
174
+ clientID: getClientID(state),
175
+ changes: collab.nextUpdate.changes,
176
+ effects: collab.nextUpdate.effects
177
+ };
178
+ }
179
+ collab_1.sendableUpdate = sendableUpdate;
180
+ function hasUnsentUpdate(state) {
181
+ let collab = state.field(collabField);
182
+ return !!(collab.nextUpdate || collab.openUpdate);
183
+ }
184
+ collab_1.hasUnsentUpdate = hasUnsentUpdate;
185
+ function getSyncedVersion(state) {
186
+ return state.field(collabField).version;
187
+ }
188
+ collab_1.getSyncedVersion = getSyncedVersion;
189
+ function getClientID(state) {
190
+ return state.facet(collabConfig).clientID;
191
+ }
192
+ collab_1.getClientID = getClientID;
193
+ function transformUpdate(update, over, corrections) {
194
+ if (!over.length)
195
+ return update;
196
+ let { clientID, version, changes, effects } = update;
197
+ for (let other of over) {
198
+ if (other.clientID == clientID)
199
+ return null;
200
+ if (effects && effects.length)
201
+ effects = Transaction.Effect.mapEffects(effects, other.changes.transform(other.doc, changes, true));
202
+ changes = changes.transform(other.doc, other.changes);
203
+ version++;
204
+ }
205
+ if (corrections && corrections.length) {
206
+ let corrected = Correction.check(changes, changes.apply(over[over.length - 1].doc), corrections);
207
+ if (corrected) {
208
+ changes = changes.compose(corrected);
209
+ if (effects)
210
+ effects = Transaction.Effect.mapEffects(effects, corrected);
211
+ }
212
+ }
213
+ return { clientID, version, changes, effects };
214
+ }
215
+ collab_1.transformUpdate = transformUpdate;
216
+ ;return collab})(collab);
217
+
218
+ export { collab };