@nerd-bible/wordgard 0.3.3
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 +22 -0
- package/README.md +24 -0
- package/dist/collab.d.ts +105 -0
- package/dist/collab.js +218 -0
- package/dist/command.d.ts +796 -0
- package/dist/command.js +1451 -0
- package/dist/doc.d.ts +2210 -0
- package/dist/doc.js +3814 -0
- package/dist/editor.d.ts +1920 -0
- package/dist/editor.js +7154 -0
- package/dist/history.d.ts +90 -0
- package/dist/history.js +278 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/phrases.d.ts +90 -0
- package/dist/phrases.js +141 -0
- package/dist/schema.d.ts +579 -0
- package/dist/schema.js +1341 -0
- package/dist/state.d.ts +1388 -0
- package/dist/state.js +2024 -0
- package/dist/table.d.ts +215 -0
- package/dist/table.js +1302 -0
- package/dist/types.d.ts +196 -0
- package/dist/types.js +288 -0
- package/package.json +52 -0
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.
|
package/dist/collab.d.ts
ADDED
|
@@ -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 };
|