linked-rolls 0.4.0 → 0.5.0

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/lib/RollCopy.d.ts CHANGED
@@ -51,6 +51,10 @@ export interface Shift {
51
51
  }
52
52
  export declare const applyShift: (shift: Shift, copy: RollCopy) => void;
53
53
  export declare const applyStretch: (paperStretch: ObjectAssumption<PaperStretch>, copy: RollCopy) => void;
54
+ /** Takes the shift off the copy's features again, as far as one was applied. */
55
+ export declare const revertShift: (copy: RollCopy) => void;
56
+ /** Takes the stretch off the copy's features again, as far as one was applied. */
57
+ export declare const revertStretch: (copy: RollCopy) => void;
54
58
  /**
55
59
  * A date value wrapped as an assumption, so that the date
56
60
  * can be annotated with a belief about its certainty and source.
package/lib/RollCopy.js CHANGED
@@ -39,6 +39,39 @@ export const applyStretch = (paperStretch, copy) => {
39
39
  copy.ops = [...copy.ops, 'stretched'];
40
40
  copy.conditions.push(paperStretch);
41
41
  };
42
+ /** Takes the shift off the copy's features again, as far as one was applied. */
43
+ export const revertShift = (copy) => {
44
+ const shift = copy.measurements.shift;
45
+ if (!copy.ops.includes('shifted') || !shift)
46
+ return;
47
+ copy.features.forEach(feature => {
48
+ feature.horizontal.from -= shift.horizontal;
49
+ if (feature.horizontal.to) {
50
+ feature.horizontal.to -= shift.horizontal;
51
+ }
52
+ feature.vertical.from -= shift.vertical;
53
+ if (feature.vertical.to) {
54
+ feature.vertical.to -= shift.vertical;
55
+ }
56
+ });
57
+ copy.ops = copy.ops.filter(op => op !== 'shifted');
58
+ delete copy.measurements.shift;
59
+ };
60
+ const isPaperStretch = (condition) => condition.conditionType === 'paper-stretch';
61
+ /** Takes the stretch off the copy's features again, as far as one was applied. */
62
+ export const revertStretch = (copy) => {
63
+ const stretch = copy.conditions.find(isPaperStretch);
64
+ if (!copy.ops.includes('stretched') || !stretch)
65
+ return;
66
+ copy.features.forEach(feature => {
67
+ feature.horizontal.from /= stretch.factor;
68
+ if (feature.horizontal.to) {
69
+ feature.horizontal.to /= stretch.factor;
70
+ }
71
+ });
72
+ copy.ops = copy.ops.filter(op => op !== 'stretched');
73
+ copy.conditions = copy.conditions.filter(condition => !isPaperStretch(condition));
74
+ };
42
75
  /**
43
76
  * Reads the features of a copy as the tracker bar would read them.
44
77
  * Holes on a position the bar does not read carry no symbol and are
package/lib/Symbol.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface Symbol<T extends string> extends WithId {
19
19
  carriers: ReferenceAssumption[];
20
20
  }
21
21
  export declare const isSymbol: (object: any) => object is AnySymbol;
22
+ export declare const isPerforation: (symbol: object | undefined) => symbol is AnyPerforation;
22
23
  /**
23
24
  * A perforation is a symbol that is typically encoded as a single punched
24
25
  * hole or a group of punched holes in the physical carrier.
@@ -141,4 +142,5 @@ export interface Text extends Symbol<'text'> {
141
142
  * Notes and expressions are perforations; texts are carried by writings.
142
143
  */
143
144
  export type AnySymbol = Note | Expression | Text;
145
+ export type AnyPerforation = Note | Expression;
144
146
  export {};
package/lib/Symbol.js CHANGED
@@ -3,6 +3,7 @@ export const isSymbol = (object) => {
3
3
  return ('type' in object
4
4
  && (object.type === 'note' || object.type === 'expression' || object.type === 'text'));
5
5
  };
6
+ export const isPerforation = (symbol) => symbol !== undefined && 'type' in symbol && (symbol.type === 'note' || symbol.type === 'expression');
6
7
  export const placementRelations = ['alignedWith', 'before', 'after'];
7
8
  /**
8
9
  * The statements placing a perforation relative to others, alignment
@@ -0,0 +1,76 @@
1
+ import { Draft } from "immer";
2
+ import { EditionView, Path } from "./EditionView";
3
+ import { Edition } from "./Edition";
4
+ import { AnySymbol, PlacementRelation } from "./Symbol";
5
+ import { CollationTolerance } from "./Collation";
6
+ import { Edit } from "./Edit";
7
+ import { PaperStretch, RollCopy, Shift } from "./RollCopy";
8
+ import { AnyArgumentation, Certainty, ObjectAssumption } from "./Assumption";
9
+ /**
10
+ * A change to an edition, written onto an immer draft of it. One
11
+ * operation is one undo step, so an operation that has to read the
12
+ * edition first takes an `EditionView` of the state it will be
13
+ * applied to and does all its writing in the one function it returns.
14
+ */
15
+ export type EditionOp = (draft: Draft<Edition>) => void;
16
+ /**
17
+ * Puts the copy into the edition with a version of its own, which
18
+ * inserts every symbol the tracker bar reads on the copy.
19
+ */
20
+ export declare const createVersion: (siglum: string, copy: RollCopy) => EditionOp;
21
+ /** Shifts and then stretches the copy's features into line with another copy's. */
22
+ export declare const alignCopy: (copyId: string, shift: Shift, stretch: ObjectAssumption<PaperStretch>) => EditionOp;
23
+ /** Puts the copy's features back where they were measured. */
24
+ export declare const unalignCopy: (copyId: string) => EditionOp;
25
+ /** The symbols of the versions that no other copy carries. */
26
+ export declare const symbolsCarriedOnlyBy: (edition: Edition, copyId: string) => AnySymbol[];
27
+ /** Takes the features off the copy, and out of the versions with what only they carried. */
28
+ export declare const removeFeatures: (copyId: string, featureIds: readonly string[]) => EditionOp;
29
+ /**
30
+ * Takes the copy out of the edition together with the symbols only it
31
+ * carries, and with every reference the versions made to those symbols.
32
+ */
33
+ export declare const removeCopy: (copyId: string) => EditionOp;
34
+ /**
35
+ * Bases the child on the parent. A symbol of the child that collates
36
+ * with one the parent hands down adds its carriers to that symbol; the
37
+ * rest become the child's insertions, and what the parent hands down
38
+ * and the child lacks becomes its deletions.
39
+ */
40
+ export declare const connectVersions: (view: EditionView, childId: string, parentId: string, tolerance?: CollationTolerance) => EditionOp;
41
+ /**
42
+ * Folds the version's own symbols into those it inherits and collates
43
+ * with: the carriers pass over, and the insertions go.
44
+ */
45
+ export declare const collateSymbols: (view: EditionView, versionId: string, symbolIds: readonly string[], tolerance?: CollationTolerance) => EditionOp;
46
+ /**
47
+ * Makes the version stand on its own: what it inherited becomes its
48
+ * own insertions, and the link to the version it was based on goes,
49
+ * with the motivations that belonged to that derivation.
50
+ */
51
+ export declare const detachVersion: (view: EditionView, versionId: string) => EditionOp;
52
+ /** Takes the version out; whatever was based on it comes to stand on its own. */
53
+ export declare const removeVersion: (view: EditionView, versionId: string) => EditionOp;
54
+ /** Takes the symbols out of the version's own insertions, and the edits that had nothing else. */
55
+ export declare const removeSymbols: (versionId: string, symbolIds: readonly string[]) => EditionOp;
56
+ /** Moves the edits into a new version based on this one. */
57
+ export declare const deriveVersion: (versionId: string, editIds: readonly string[]) => EditionOp;
58
+ /**
59
+ * Replaces the edits with a single one carrying all their insertions
60
+ * and deletions, classified by a guess at what the exchange does.
61
+ */
62
+ export declare const mergeEdits: (view: EditionView, versionId: string, toMerge: readonly Edit[]) => EditionOp;
63
+ /** Replaces the edit with one edit per inserted and one per deleted symbol. */
64
+ export declare const splitEdit: (versionId: string, toSplit: Edit) => EditionOp;
65
+ /** States how the follower is placed relative to the reference, in place of any earlier statement. */
66
+ export declare const placePerforation: (view: EditionView, followerId: string, referenceId: string, relation: PlacementRelation) => EditionOp;
67
+ export declare const unplacePerforation: (view: EditionView, followerId: string) => EditionOp;
68
+ /** The pair is stated on `statingId` only, as the format asks. */
69
+ export declare const pairPerforations: (view: EditionView, statingId: string, partnerId: string) => EditionOp;
70
+ export declare const unpairPerforation: (view: EditionView, statingId: string) => EditionOp;
71
+ /** Annotates the assumption at the path with a belief held true, for reasons to be added. */
72
+ export declare const createBelief: (path: Path) => EditionOp;
73
+ export declare const clearBelief: (path: Path) => EditionOp;
74
+ export declare const setCertainty: (path: Path, certainty: Certainty) => EditionOp;
75
+ export declare const addReason: (path: Path, reason: AnyArgumentation) => EditionOp;
76
+ export declare const removeReason: (path: Path, index: number) => EditionOp;
@@ -0,0 +1,315 @@
1
+ import { v4 } from "uuid";
2
+ import { getAt } from "./EditionView";
3
+ import { isPerforation, placementRelations } from "./Symbol";
4
+ import { applyShift, applyStretch, asSymbols, revertShift, revertStretch } from "./RollCopy";
5
+ import { assignReference, idOf } from "./Assumption";
6
+ const noChange = () => undefined;
7
+ const onCopy = (copyId, op) => draft => {
8
+ const copy = draft.copies.find(c => c.id === copyId);
9
+ if (copy)
10
+ op(copy, draft);
11
+ };
12
+ const onVersion = (versionId, op) => draft => {
13
+ const version = draft.versions.find(v => v.id === versionId);
14
+ if (version)
15
+ op(version, draft);
16
+ };
17
+ /** The items that do not match, or the very same array when none does, so that a draft stays untouched. */
18
+ const without = (items, matches) => items.some(matches) ? items.filter(item => !matches(item)) : items;
19
+ const defaultTolerance = { toleranceStart: 5, toleranceEnd: 5 };
20
+ const insertion = (symbol) => ({ type: 'edit', id: v4(), insert: [symbol] });
21
+ const deletion = (symbolId) => ({ type: 'edit', id: v4(), delete: [symbolId] });
22
+ const isEmpty = (edit) => !edit.insert?.length && !edit.delete?.length;
23
+ const insertedIn = (versions) => versions.flatMap(version => version.edits).flatMap(edit => edit.insert ?? []);
24
+ /** Runs the change over the version's edits and drops those it emptied, leaving edits that were empty before alone. */
25
+ const editing = (version, change) => {
26
+ const emptyBefore = new Set(version.edits.filter(isEmpty).map(edit => edit.id));
27
+ version.edits.forEach(change);
28
+ version.edits = without(version.edits, edit => isEmpty(edit) && !emptyBefore.has(edit.id));
29
+ };
30
+ /** Takes the symbols out of the version's own insertions. */
31
+ const dropInsertions = (version, symbolIds) => editing(version, edit => {
32
+ if (edit.insert)
33
+ edit.insert = without(edit.insert, symbol => symbolIds.has(symbol.id));
34
+ });
35
+ /**
36
+ * Puts the copy into the edition with a version of its own, which
37
+ * inserts every symbol the tracker bar reads on the copy.
38
+ */
39
+ export const createVersion = (siglum, copy) => draft => {
40
+ draft.copies.push(copy);
41
+ draft.versions.push({
42
+ type: 'Version',
43
+ id: v4(),
44
+ siglum,
45
+ versionType: 'edition',
46
+ edits: asSymbols(copy.features).map(insertion),
47
+ motivations: []
48
+ });
49
+ };
50
+ /** Shifts and then stretches the copy's features into line with another copy's. */
51
+ export const alignCopy = (copyId, shift, stretch) => onCopy(copyId, copy => {
52
+ applyShift(shift, copy);
53
+ applyStretch(stretch, copy);
54
+ });
55
+ /** Puts the copy's features back where they were measured. */
56
+ export const unalignCopy = (copyId) => onCopy(copyId, copy => {
57
+ revertStretch(copy);
58
+ revertShift(copy);
59
+ });
60
+ const featureIdsOf = (copy) => new Set(copy.features.map(feature => feature.id));
61
+ /**
62
+ * A symbol every carrier of which lies among the features loses its
63
+ * evidence with them. A symbol without carriers, such as a label,
64
+ * stands on its own.
65
+ */
66
+ const carriedOnlyOn = (features) => (symbol) => symbol.carriers.length > 0 && symbol.carriers.every(carrier => features.has(idOf(carrier)));
67
+ /** The symbols of the versions that no other copy carries. */
68
+ export const symbolsCarriedOnlyBy = (edition, copyId) => {
69
+ const copy = edition.copies.find(c => c.id === copyId);
70
+ return copy ? insertedIn(edition.versions).filter(carriedOnlyOn(featureIdsOf(copy))) : [];
71
+ };
72
+ const references = [...placementRelations, 'pairedWith'];
73
+ const forgetPerforations = (perforation, dropped) => references
74
+ .filter(relation => {
75
+ const reference = perforation[relation];
76
+ return reference && dropped.has(idOf(reference));
77
+ })
78
+ .forEach(relation => { delete perforation[relation]; });
79
+ const forgetCarriers = (symbol, features, dropped) => {
80
+ symbol.carriers = without(symbol.carriers, carrier => features.has(idOf(carrier)));
81
+ if (isPerforation(symbol))
82
+ forgetPerforations(symbol, dropped);
83
+ };
84
+ const forgetFeaturesInEdit = (edit, features, dropped) => {
85
+ if (edit.insert) {
86
+ edit.insert = without(edit.insert, symbol => dropped.has(symbol.id));
87
+ edit.insert.forEach(symbol => forgetCarriers(symbol, features, dropped));
88
+ }
89
+ if (edit.delete) {
90
+ edit.delete = without(edit.delete, id => dropped.has(id));
91
+ }
92
+ };
93
+ /**
94
+ * Strikes the features from the versions: their carriers go, a symbol
95
+ * that had no other carrier goes with them, and so does every
96
+ * reference the versions made to such a symbol.
97
+ */
98
+ const forgetFeatures = (draft, features) => {
99
+ const dropped = new Set(insertedIn(draft.versions).filter(carriedOnlyOn(features)).map(symbol => symbol.id));
100
+ draft.versions.forEach(version => editing(version, edit => forgetFeaturesInEdit(edit, features, dropped)));
101
+ };
102
+ /** Takes the features off the copy, and out of the versions with what only they carried. */
103
+ export const removeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
104
+ const features = new Set(featureIds);
105
+ copy.features = without(copy.features, feature => features.has(feature.id));
106
+ forgetFeatures(draft, features);
107
+ });
108
+ /**
109
+ * Takes the copy out of the edition together with the symbols only it
110
+ * carries, and with every reference the versions made to those symbols.
111
+ */
112
+ export const removeCopy = (copyId) => onCopy(copyId, (copy, draft) => {
113
+ forgetFeatures(draft, featureIdsOf(copy));
114
+ draft.copies = draft.copies.filter(c => c.id !== copyId);
115
+ });
116
+ /** Each of the version's own symbols with every inherited symbol it collates with. */
117
+ const collationsOf = (view, own, inherited, tolerance) => own.flatMap(symbol => inherited
118
+ .filter(candidate => view.isCollatable(symbol, candidate, tolerance))
119
+ .map(counterpart => ({ symbol, counterpart })));
120
+ /** The carriers of each collated symbol pass to its counterpart. */
121
+ const handOverCarriers = (view, draft, collations) => collations.forEach(({ symbol, counterpart }) => {
122
+ const path = view.getPath(counterpart.id);
123
+ const target = path && getAt(path, draft);
124
+ target?.carriers.push(...symbol.carriers);
125
+ });
126
+ /**
127
+ * Bases the child on the parent. A symbol of the child that collates
128
+ * with one the parent hands down adds its carriers to that symbol; the
129
+ * rest become the child's insertions, and what the parent hands down
130
+ * and the child lacks becomes its deletions.
131
+ */
132
+ export const connectVersions = (view, childId, parentId, tolerance = defaultTolerance) => {
133
+ const inherited = view.snapshot(parentId);
134
+ const own = view.snapshot(childId);
135
+ const collations = collationsOf(view, own, inherited, tolerance);
136
+ const collated = new Set(collations.map(({ symbol }) => symbol.id));
137
+ const matched = new Set(collations.map(({ counterpart }) => counterpart.id));
138
+ const edits = [
139
+ ...own.filter(symbol => !collated.has(symbol.id)).map(insertion),
140
+ ...inherited.filter(symbol => !matched.has(symbol.id)).map(symbol => deletion(symbol.id))
141
+ ];
142
+ return onVersion(childId, (child, draft) => {
143
+ handOverCarriers(view, draft, collations);
144
+ child.edits = edits;
145
+ child.basedOn = assignReference(parentId);
146
+ });
147
+ };
148
+ /**
149
+ * Folds the version's own symbols into those it inherits and collates
150
+ * with: the carriers pass over, and the insertions go.
151
+ */
152
+ export const collateSymbols = (view, versionId, symbolIds, tolerance = defaultTolerance) => {
153
+ const version = view.get(versionId);
154
+ if (!version?.basedOn)
155
+ return noChange;
156
+ const chosen = new Set(symbolIds);
157
+ const own = insertedIn([version]).filter(symbol => chosen.has(symbol.id));
158
+ const collations = collationsOf(view, own, view.snapshot(idOf(version.basedOn)), tolerance);
159
+ const collated = new Set(collations.map(({ symbol }) => symbol.id));
160
+ return onVersion(versionId, (version, draft) => {
161
+ handOverCarriers(view, draft, collations);
162
+ dropInsertions(version, collated);
163
+ });
164
+ };
165
+ /**
166
+ * Makes the version stand on its own: what it inherited becomes its
167
+ * own insertions, and the link to the version it was based on goes,
168
+ * with the motivations that belonged to that derivation.
169
+ */
170
+ export const detachVersion = (view, versionId) => {
171
+ const edits = view.snapshot(versionId).map(insertion);
172
+ return onVersion(versionId, version => {
173
+ version.edits = edits;
174
+ delete version.basedOn;
175
+ version.motivations = [];
176
+ });
177
+ };
178
+ /** Takes the version out; whatever was based on it comes to stand on its own. */
179
+ export const removeVersion = (view, versionId) => {
180
+ const detachments = view.edition.versions
181
+ .filter(version => version.basedOn && idOf(version.basedOn) === versionId)
182
+ .map(version => detachVersion(view, version.id));
183
+ return draft => {
184
+ detachments.forEach(detach => detach(draft));
185
+ draft.versions = without(draft.versions, version => version.id === versionId);
186
+ };
187
+ };
188
+ /** Takes the symbols out of the version's own insertions, and the edits that had nothing else. */
189
+ export const removeSymbols = (versionId, symbolIds) => onVersion(versionId, version => dropInsertions(version, new Set(symbolIds)));
190
+ /** Moves the edits into a new version based on this one. */
191
+ export const deriveVersion = (versionId, editIds) => onVersion(versionId, (version, draft) => {
192
+ const chosen = new Set(editIds);
193
+ const moved = version.edits.filter(edit => chosen.has(edit.id));
194
+ version.edits = without(version.edits, edit => chosen.has(edit.id));
195
+ draft.versions.push({
196
+ type: 'Version',
197
+ id: v4(),
198
+ siglum: `${version.siglum}_derived`,
199
+ versionType: 'unicum',
200
+ basedOn: assignReference(versionId),
201
+ edits: moved,
202
+ motivations: []
203
+ });
204
+ });
205
+ const sameSequence = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
206
+ const expressionTypesOf = (symbols) => symbols.filter((symbol) => symbol.type === 'expression').map(symbol => symbol.expressionType);
207
+ const accents = [['SlowCrescendoOn', 'SlowCrescendoOff'], ['ForzandoOn', 'ForzandoOff']];
208
+ const lengthOf = (span) => span.to - span.from;
209
+ /** Shorten or prolong, where the inserted symbol starts about where the deleted one did. */
210
+ const replacementType = (view, inserted, deleted) => {
211
+ const after = view.dimensionOf(inserted)?.horizontal;
212
+ const before = view.dimensionOf(deleted)?.horizontal;
213
+ if (!after || !before || Math.abs(after.from - before.from) >= 5)
214
+ return undefined;
215
+ return lengthOf(after) < lengthOf(before) ? 'shorten' : 'prolong';
216
+ };
217
+ /** A guess at what an edit does, from the symbols it exchanges. */
218
+ const guessEditType = (view, edit) => {
219
+ const inserts = edit.insert ?? [];
220
+ const deletes = view.getAll(edit.delete ?? []);
221
+ const inserted = expressionTypesOf(inserts);
222
+ const deleted = expressionTypesOf(deletes);
223
+ if (deleted.length === 0 && accents.some(accent => sameSequence(inserted, accent)))
224
+ return 'additional-accent';
225
+ if (inserted.length > 1 && sameSequence(inserted, deleted))
226
+ return 'shift';
227
+ if (inserted.length === 0 && deleted.length === 1)
228
+ return 'remove-redundancy';
229
+ if (inserts.length === 1 && deletes.length === 1)
230
+ return replacementType(view, inserts[0], deletes[0]) ?? 'correct-error';
231
+ return 'correct-error';
232
+ };
233
+ /**
234
+ * Replaces the edits with a single one carrying all their insertions
235
+ * and deletions, classified by a guess at what the exchange does.
236
+ */
237
+ export const mergeEdits = (view, versionId, toMerge) => {
238
+ if (toMerge.length === 0)
239
+ return noChange;
240
+ const merged = {
241
+ ...toMerge[0],
242
+ id: v4(),
243
+ insert: toMerge.flatMap(edit => edit.insert ?? []),
244
+ delete: toMerge.flatMap(edit => edit.delete ?? [])
245
+ };
246
+ merged.editType = guessEditType(view, merged);
247
+ const mergedIds = new Set(toMerge.map(edit => edit.id));
248
+ return onVersion(versionId, version => {
249
+ version.edits = [...version.edits.filter(edit => !mergedIds.has(edit.id)), merged];
250
+ });
251
+ };
252
+ /** Replaces the edit with one edit per inserted and one per deleted symbol. */
253
+ export const splitEdit = (versionId, toSplit) => {
254
+ const parts = [
255
+ ...(toSplit.insert ?? []).map(insertion),
256
+ ...(toSplit.delete ?? []).map(deletion)
257
+ ];
258
+ return onVersion(versionId, version => {
259
+ version.edits = [...version.edits.filter(edit => edit.id !== toSplit.id), ...parts];
260
+ });
261
+ };
262
+ /**
263
+ * Runs the change on the perforation the view locates by id, in whichever
264
+ * version inserted it. A statement made there holds in every version
265
+ * that carries the perforation.
266
+ */
267
+ const onPerforation = (view, id, op) => draft => {
268
+ const path = view.getPath(id);
269
+ const symbol = path && getAt(path, draft);
270
+ if (isPerforation(symbol))
271
+ op(symbol);
272
+ };
273
+ const clearPlacement = (perforation) => placementRelations.forEach(relation => { delete perforation[relation]; });
274
+ /** States how the follower is placed relative to the reference, in place of any earlier statement. */
275
+ export const placePerforation = (view, followerId, referenceId, relation) => onPerforation(view, followerId, perforation => {
276
+ clearPlacement(perforation);
277
+ perforation[relation] = assignReference(referenceId);
278
+ });
279
+ export const unplacePerforation = (view, followerId) => onPerforation(view, followerId, clearPlacement);
280
+ /** The pair is stated on `statingId` only, as the format asks. */
281
+ export const pairPerforations = (view, statingId, partnerId) => onPerforation(view, statingId, perforation => {
282
+ perforation.pairedWith = assignReference(partnerId);
283
+ });
284
+ export const unpairPerforation = (view, statingId) => onPerforation(view, statingId, perforation => {
285
+ delete perforation.pairedWith;
286
+ });
287
+ const onAssumptionAt = (path, op) => draft => {
288
+ const assumption = getAt(path, draft);
289
+ if (assumption)
290
+ op(assumption);
291
+ };
292
+ const onBeliefAt = (path, op) => onAssumptionAt(path, assumption => {
293
+ const belief = assumption['@annotation']?.belief;
294
+ if (belief)
295
+ op(belief);
296
+ });
297
+ /** Annotates the assumption at the path with a belief held true, for reasons to be added. */
298
+ export const createBelief = (path) => onAssumptionAt(path, assumption => {
299
+ assumption['@annotation'] = {
300
+ id: v4(),
301
+ belief: { type: 'belief', id: v4(), certainty: 'true', reasons: [] }
302
+ };
303
+ });
304
+ export const clearBelief = (path) => onAssumptionAt(path, assumption => {
305
+ delete assumption['@annotation'];
306
+ });
307
+ export const setCertainty = (path, certainty) => onBeliefAt(path, belief => {
308
+ belief.certainty = certainty;
309
+ });
310
+ export const addReason = (path, reason) => onBeliefAt(path, belief => {
311
+ belief.reasons.push(reason);
312
+ });
313
+ export const removeReason = (path, index) => onBeliefAt(path, belief => {
314
+ belief.reasons.splice(index, 1);
315
+ });
package/lib/index.d.ts CHANGED
@@ -16,6 +16,6 @@ export * from './TrackerBar';
16
16
  export * from './TrackCalibration';
17
17
  export * from './alignFeatures';
18
18
  export * from './EditionView';
19
- export * from './Plan';
19
+ export * from './editionOps';
20
20
  export * from './validate';
21
21
  export * from './utils';
package/lib/index.js CHANGED
@@ -16,6 +16,6 @@ export * from './TrackerBar';
16
16
  export * from './TrackCalibration';
17
17
  export * from './alignFeatures';
18
18
  export * from './EditionView';
19
- export * from './Plan';
19
+ export * from './editionOps';
20
20
  export * from './validate';
21
21
  export * from './utils';
package/lib/schema.json CHANGED
@@ -1189,7 +1189,7 @@
1189
1189
  ],
1190
1190
  "type": "object"
1191
1191
  },
1192
- "ObjectAssumption<alias-731470504-73739-73887-731470504-0-217694<def-interface-src_Symbol.ts-5565-5929-src_Symbol.ts-0-6137400334313,\"carriers\">>": {
1192
+ "ObjectAssumption<alias-731470504-73739-73887-731470504-0-217694<def-interface-src_Symbol.ts-5758-6122-src_Symbol.ts-0-6378400334313,\"carriers\">>": {
1193
1193
  "description": "An object assumption wraps a complex object with an optional annotation. Used for structured values (e.g. persons, conditions) whose properties may be uncertain.",
1194
1194
  "properties": {
1195
1195
  "@annotation": {
@@ -1760,7 +1760,7 @@
1760
1760
  "description": "The method by which this writing was produced (e.g. through print, handwriting, or stamping). [ontology: crm:P2 has type]"
1761
1761
  },
1762
1762
  "transcription": {
1763
- "$ref": "#/definitions/ObjectAssumption%3Calias-731470504-73739-73887-731470504-0-217694%3Cdef-interface-src_Symbol.ts-5565-5929-src_Symbol.ts-0-6137400334313%2C%22carriers%22%3E%3E",
1763
+ "$ref": "#/definitions/ObjectAssumption%3Calias-731470504-73739-73887-731470504-0-217694%3Cdef-interface-src_Symbol.ts-5758-6122-src_Symbol.ts-0-6378400334313%2C%22carriers%22%3E%3E",
1764
1764
  "description": "A transcription of the text content of the writing. This is an object assumption so that the transcription can be annotated with a belief about its correctness. [ontology: crm:P128 carries]"
1765
1765
  },
1766
1766
  "vertical": {
package/lib/validate.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import Ajv from "ajv";
2
2
  import * as schema from "./schema.json";
3
3
  import { idOf } from "./Assumption";
4
- import { pairsAmong, placementsOf } from "./Symbol";
4
+ import { isPerforation, pairsAmong, placementsOf } from "./Symbol";
5
5
  const ajv = new Ajv({
6
6
  strict: false,
7
7
  formats: {
@@ -15,7 +15,6 @@ const missingReference = {
15
15
  before: 'before-reference-missing',
16
16
  after: 'after-reference-missing'
17
17
  };
18
- const isPerforation = (symbol) => symbol.type !== 'text';
19
18
  const problemsIn = (version, perforations) => {
20
19
  const ids = new Set(perforations.map(p => p.id));
21
20
  const report = (symbol, problem) => ({ version, symbol, problem });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system",
5
5
  "license": "MIT",
6
6
  "repository": {
package/lib/Plan.d.ts DELETED
@@ -1,190 +0,0 @@
1
- import { Draft } from "immer";
2
- import { EditionView } from "./EditionView";
3
- import { Edition } from "./Edition";
4
- import { CollationTolerance } from "./Collation";
5
- import { Edit } from "./Edit";
6
- import { RollCopy } from "./RollCopy";
7
- export type EditionOp = (d: Draft<Edition>) => void;
8
- export interface Plan {
9
- build(): EditionOp[];
10
- setView: (view: EditionView) => void;
11
- }
12
- export declare const isPlan: (obj: any) => obj is Plan;
13
- export declare abstract class BasePlan implements Plan {
14
- protected view: EditionView | null;
15
- setView: (view: EditionView) => void;
16
- abstract build(): EditionOp[];
17
- }
18
- /**
19
- * Walks through the versions. If it finds a symbol that is still
20
- * part of the tradition (i.e. it is included in the current snapshot)
21
- * and is equivalent with the given symbol, it will add the feature
22
- * carrying the symbol to the collated symbol. Otherwise, the
23
- * given symbol will be added to the current version's insertions.
24
- *
25
- * All symbols of the tradition that are not included in the given
26
- * symbols are considered to be deleted.
27
- *
28
- * @param creation
29
- * @param symbols
30
- * @returns
31
- */
32
- export declare class ConnectVersions extends BasePlan {
33
- private childId;
34
- private parentId;
35
- private tolerance;
36
- constructor(childId: string, parentId: string, tolerance?: CollationTolerance);
37
- build(): EditionOp[];
38
- }
39
- /**
40
- * Walks through the versions. If it finds a symbol that is still
41
- * part of the tradition (i.e. it is included in the current snapshot)
42
- * and is equivalent with the given symbol, it will add the feature
43
- * carrying the symbol to the collated symbol. Otherwise, the
44
- * given symbol will be added to the current version's insertions.
45
- *
46
- * All symbols of the tradition that are not included in the given
47
- * symbols are considered to be deleted.
48
- *
49
- * @param creation
50
- * @param symbols
51
- * @returns
52
- */
53
- export declare class CreateVersion extends BasePlan {
54
- private siglum;
55
- private copy;
56
- constructor(siglum: string, copy: RollCopy);
57
- build(): EditionOp[];
58
- }
59
- /**
60
- * Creates a new version based on the given version
61
- * calculating the effect of a cover on existing symbols.
62
-
63
- export class CoverPerforation extends BasePlan {
64
- constructor(
65
- private copyId: string,
66
- private versionId: string,
67
- private coverDimension: { horizontal: HorizontalSpan, vertical: VerticalSpan },
68
- private material: string | undefined = undefined,
69
- ) {
70
- super()
71
- }
72
-
73
- build(): EditionOp[] {
74
- const view = this.view
75
- if (!view) return []
76
-
77
- return [
78
- (draft: Draft<Edition>): void => {
79
- const version = draft.versions.find(v => v.id === this.versionId)
80
- const copy = draft.copies.find(c => c.id === this.copyId)
81
- if (!version || !copy) return
82
-
83
- // find perforations in the snapshot that
84
- // overlap with the cover
85
- const overlappingSymbols =
86
- view.snapshot(this.versionId)
87
- .filter(symbol => {
88
- const dimension = view.dimensionOf(symbol)
89
- if (!dimension) return false
90
-
91
- return (
92
- (symbol.type === 'note' || symbol.type === 'expression') &&
93
- overlaps(dimension, this.coverDimension)
94
- )
95
- })
96
-
97
- if (!overlappingSymbols.length) return undefined
98
-
99
- const deepClone = JSON.parse(JSON.stringify(overlappingSymbols)) as AnySymbol[]
100
-
101
- for (const symbol of deepClone) {
102
- //symbol.id = v4();
103
- const dimension = structuredClone(view.dimensionOf(symbol))
104
- if (!dimension) continue
105
-
106
- // check if the cover partially covers the beginning
107
- if (dimension.horizontal.from >= this.coverDimension.horizontal.from &&
108
- dimension.horizontal.from <= this.coverDimension.horizontal.to
109
- ) {
110
- dimension.horizontal.from = this.coverDimension.horizontal.to
111
- }
112
-
113
- // check if the cover partially covers the ending
114
- if (dimension.horizontal.to >= this.coverDimension.horizontal.from &&
115
- dimension.horizontal.to <= this.coverDimension.horizontal.to
116
- ) {
117
- dimension.horizontal.to = this.coverDimension.horizontal.from
118
- }
119
-
120
- const newFeature: RollFeature = {
121
- ...dimension,
122
- id: v4(),
123
- }
124
-
125
- copy.features.push(newFeature)
126
- symbol.id = `symbol-${v4().slice(0, 8)}`
127
- symbol.carriers = [assignReference(newFeature.id)]
128
- }
129
-
130
- const coverId = `cover-${v4().slice(0, 8)}`
131
- copy.features.push({
132
- ...this.coverDimension,
133
- id: coverId,
134
- })
135
-
136
- const edit: Edit = {
137
- type: 'edit',
138
- id: v4(),
139
- delete: overlappingSymbols.map(s => s.id),
140
- insert: deepClone,
141
- intentionOf: [{
142
- type: 'cover',
143
- id: v4(),
144
- note: this.material,
145
- carriers: [assignReference(coverId)],
146
- }]
147
- }
148
-
149
- draft.versions.push({
150
- edits: [edit],
151
- id: v4(),
152
- basedOn: assignReference(version.id),
153
- siglum: version.siglum + ' rev',
154
- type: 'authorised-revision',
155
- motivations: [
156
- {
157
- type: 'motivation',
158
- note: 'Stanzfehler korrigiert',
159
- id: v4(),
160
- }
161
- ],
162
- })
163
- }]
164
- }
165
- }
166
- */
167
- export declare class DetachVersion extends BasePlan {
168
- private versionId;
169
- constructor(versionId: string);
170
- build(): EditionOp[];
171
- }
172
- export declare class RemoveFeature extends BasePlan {
173
- private copyID;
174
- private featureIDs;
175
- constructor(copyID: string, featureIDs: string[]);
176
- build(): EditionOp[];
177
- }
178
- export declare class MergeEdits extends BasePlan {
179
- private versionId;
180
- private toMerge;
181
- constructor(versionId: string, toMerge: Edit[]);
182
- private guessMotivation;
183
- build(): EditionOp[];
184
- }
185
- export declare class SplitEdit extends BasePlan {
186
- private versionId;
187
- private toSplit;
188
- constructor(versionId: string, toSplit: Edit);
189
- build(): EditionOp[];
190
- }
package/lib/Plan.js DELETED
@@ -1,556 +0,0 @@
1
- import { getAt } from "./EditionView";
2
- import { v4 } from "uuid";
3
- import { asSymbols } from "./RollCopy";
4
- import { assignReference, idOf } from "./Assumption";
5
- export const isPlan = (obj) => {
6
- return obj && typeof obj.build === 'function' && typeof obj.setView === 'function';
7
- };
8
- export class BasePlan {
9
- constructor() {
10
- Object.defineProperty(this, "view", {
11
- enumerable: true,
12
- configurable: true,
13
- writable: true,
14
- value: null
15
- });
16
- Object.defineProperty(this, "setView", {
17
- enumerable: true,
18
- configurable: true,
19
- writable: true,
20
- value: (view) => {
21
- this.view = view;
22
- }
23
- });
24
- }
25
- }
26
- /**
27
- * Walks through the versions. If it finds a symbol that is still
28
- * part of the tradition (i.e. it is included in the current snapshot)
29
- * and is equivalent with the given symbol, it will add the feature
30
- * carrying the symbol to the collated symbol. Otherwise, the
31
- * given symbol will be added to the current version's insertions.
32
- *
33
- * All symbols of the tradition that are not included in the given
34
- * symbols are considered to be deleted.
35
- *
36
- * @param creation
37
- * @param symbols
38
- * @returns
39
- */
40
- export class ConnectVersions extends BasePlan {
41
- constructor(childId, parentId, tolerance = {
42
- toleranceEnd: 5,
43
- toleranceStart: 5
44
- }) {
45
- super();
46
- Object.defineProperty(this, "childId", {
47
- enumerable: true,
48
- configurable: true,
49
- writable: true,
50
- value: childId
51
- });
52
- Object.defineProperty(this, "parentId", {
53
- enumerable: true,
54
- configurable: true,
55
- writable: true,
56
- value: parentId
57
- });
58
- Object.defineProperty(this, "tolerance", {
59
- enumerable: true,
60
- configurable: true,
61
- writable: true,
62
- value: tolerance
63
- });
64
- }
65
- build() {
66
- if (!this.view)
67
- return [];
68
- const view = this.view;
69
- const result = [];
70
- const parentSnapshot = view.snapshot(this.parentId);
71
- const childSnapshot = view.snapshot(this.childId);
72
- const treatedSymbols = [];
73
- // can the symbol be collated with any of the
74
- // symbols of the tradition, i.e. the ones
75
- // included in the current snapshot?
76
- const insertions = [...childSnapshot];
77
- for (const symbol of childSnapshot) {
78
- parentSnapshot
79
- .filter(toCompare => view.isCollatable(symbol, toCompare, this.tolerance))
80
- .forEach(parentSymbol => {
81
- result.push(draft => {
82
- const symbolPath = view.getPath(parentSymbol.id);
83
- if (!symbolPath)
84
- return;
85
- const correspSymbol = getAt(symbolPath, draft);
86
- if (!correspSymbol)
87
- return;
88
- correspSymbol.carriers.push(...symbol.carriers);
89
- });
90
- insertions.splice(insertions.indexOf(symbol), 1);
91
- treatedSymbols.push(parentSymbol);
92
- });
93
- }
94
- const edits = insertions.map((symbol) => {
95
- return {
96
- type: 'edit',
97
- id: v4(),
98
- insert: [symbol],
99
- delete: [],
100
- };
101
- });
102
- const deletions = parentSnapshot.filter(sym => {
103
- return !treatedSymbols.includes(sym);
104
- });
105
- for (const symbol of deletions) {
106
- edits.push({
107
- type: 'edit',
108
- id: v4(),
109
- insert: [],
110
- delete: [symbol.id],
111
- });
112
- }
113
- result.push(draft => {
114
- const v = draft.versions.find(v => v.id === this.childId);
115
- if (!v)
116
- return;
117
- v.edits = edits;
118
- v.basedOn = assignReference(this.parentId);
119
- });
120
- return result;
121
- }
122
- }
123
- /**
124
- * Walks through the versions. If it finds a symbol that is still
125
- * part of the tradition (i.e. it is included in the current snapshot)
126
- * and is equivalent with the given symbol, it will add the feature
127
- * carrying the symbol to the collated symbol. Otherwise, the
128
- * given symbol will be added to the current version's insertions.
129
- *
130
- * All symbols of the tradition that are not included in the given
131
- * symbols are considered to be deleted.
132
- *
133
- * @param creation
134
- * @param symbols
135
- * @returns
136
- */
137
- export class CreateVersion extends BasePlan {
138
- constructor(siglum, copy) {
139
- super();
140
- Object.defineProperty(this, "siglum", {
141
- enumerable: true,
142
- configurable: true,
143
- writable: true,
144
- value: siglum
145
- });
146
- Object.defineProperty(this, "copy", {
147
- enumerable: true,
148
- configurable: true,
149
- writable: true,
150
- value: copy
151
- });
152
- }
153
- build() {
154
- const view = this.view;
155
- if (!view)
156
- return [];
157
- const result = [];
158
- result.push(draft => {
159
- draft.copies.push(this.copy);
160
- const newVersion = {
161
- type: 'Version',
162
- siglum: this.siglum,
163
- id: v4(),
164
- edits: asSymbols(this.copy.features).map((symbol) => {
165
- return {
166
- type: 'edit',
167
- id: v4(),
168
- insert: [symbol],
169
- delete: [],
170
- };
171
- }),
172
- versionType: 'edition',
173
- motivations: []
174
- };
175
- draft.versions.push(newVersion);
176
- });
177
- return result;
178
- }
179
- }
180
- /**
181
- * Creates a new version based on the given version
182
- * calculating the effect of a cover on existing symbols.
183
-
184
- export class CoverPerforation extends BasePlan {
185
- constructor(
186
- private copyId: string,
187
- private versionId: string,
188
- private coverDimension: { horizontal: HorizontalSpan, vertical: VerticalSpan },
189
- private material: string | undefined = undefined,
190
- ) {
191
- super()
192
- }
193
-
194
- build(): EditionOp[] {
195
- const view = this.view
196
- if (!view) return []
197
-
198
- return [
199
- (draft: Draft<Edition>): void => {
200
- const version = draft.versions.find(v => v.id === this.versionId)
201
- const copy = draft.copies.find(c => c.id === this.copyId)
202
- if (!version || !copy) return
203
-
204
- // find perforations in the snapshot that
205
- // overlap with the cover
206
- const overlappingSymbols =
207
- view.snapshot(this.versionId)
208
- .filter(symbol => {
209
- const dimension = view.dimensionOf(symbol)
210
- if (!dimension) return false
211
-
212
- return (
213
- (symbol.type === 'note' || symbol.type === 'expression') &&
214
- overlaps(dimension, this.coverDimension)
215
- )
216
- })
217
-
218
- if (!overlappingSymbols.length) return undefined
219
-
220
- const deepClone = JSON.parse(JSON.stringify(overlappingSymbols)) as AnySymbol[]
221
-
222
- for (const symbol of deepClone) {
223
- //symbol.id = v4();
224
- const dimension = structuredClone(view.dimensionOf(symbol))
225
- if (!dimension) continue
226
-
227
- // check if the cover partially covers the beginning
228
- if (dimension.horizontal.from >= this.coverDimension.horizontal.from &&
229
- dimension.horizontal.from <= this.coverDimension.horizontal.to
230
- ) {
231
- dimension.horizontal.from = this.coverDimension.horizontal.to
232
- }
233
-
234
- // check if the cover partially covers the ending
235
- if (dimension.horizontal.to >= this.coverDimension.horizontal.from &&
236
- dimension.horizontal.to <= this.coverDimension.horizontal.to
237
- ) {
238
- dimension.horizontal.to = this.coverDimension.horizontal.from
239
- }
240
-
241
- const newFeature: RollFeature = {
242
- ...dimension,
243
- id: v4(),
244
- }
245
-
246
- copy.features.push(newFeature)
247
- symbol.id = `symbol-${v4().slice(0, 8)}`
248
- symbol.carriers = [assignReference(newFeature.id)]
249
- }
250
-
251
- const coverId = `cover-${v4().slice(0, 8)}`
252
- copy.features.push({
253
- ...this.coverDimension,
254
- id: coverId,
255
- })
256
-
257
- const edit: Edit = {
258
- type: 'edit',
259
- id: v4(),
260
- delete: overlappingSymbols.map(s => s.id),
261
- insert: deepClone,
262
- intentionOf: [{
263
- type: 'cover',
264
- id: v4(),
265
- note: this.material,
266
- carriers: [assignReference(coverId)],
267
- }]
268
- }
269
-
270
- draft.versions.push({
271
- edits: [edit],
272
- id: v4(),
273
- basedOn: assignReference(version.id),
274
- siglum: version.siglum + ' rev',
275
- type: 'authorised-revision',
276
- motivations: [
277
- {
278
- type: 'motivation',
279
- note: 'Stanzfehler korrigiert',
280
- id: v4(),
281
- }
282
- ],
283
- })
284
- }]
285
- }
286
- }
287
- */
288
- export class DetachVersion extends BasePlan {
289
- constructor(versionId) {
290
- super();
291
- Object.defineProperty(this, "versionId", {
292
- enumerable: true,
293
- configurable: true,
294
- writable: true,
295
- value: versionId
296
- });
297
- }
298
- build() {
299
- if (!this.view)
300
- return [];
301
- const snapshot = this.view.snapshot(this.versionId);
302
- const newEdits = snapshot.map((symbol) => ({
303
- type: 'edit',
304
- id: v4(),
305
- insert: [symbol],
306
- delete: [],
307
- }));
308
- return [draft => {
309
- const v = draft.versions.find(ver => ver.id === this.versionId);
310
- if (!v)
311
- return;
312
- v.edits = newEdits;
313
- delete v.basedOn;
314
- v.motivations = [];
315
- }];
316
- }
317
- }
318
- export class RemoveFeature extends BasePlan {
319
- constructor(copyID, featureIDs) {
320
- super();
321
- Object.defineProperty(this, "copyID", {
322
- enumerable: true,
323
- configurable: true,
324
- writable: true,
325
- value: copyID
326
- });
327
- Object.defineProperty(this, "featureIDs", {
328
- enumerable: true,
329
- configurable: true,
330
- writable: true,
331
- value: featureIDs
332
- });
333
- }
334
- build() {
335
- const view = this.view;
336
- if (!view)
337
- return [];
338
- return [
339
- (draft) => {
340
- const copy = draft.copies.find(c => c.id === this.copyID);
341
- if (!copy)
342
- return;
343
- copy.features = copy.features.filter(f => !this.featureIDs.includes(f.id));
344
- this.featureIDs.forEach(featureId => {
345
- // expect a path of format:
346
- // ['versions', index, 'edits', index, 'insert', index, 'carriers', index, 'id']
347
- const carrierPath = view.linksTo(featureId).at(0);
348
- if (!carrierPath)
349
- return;
350
- // expect to find the parent symbol at:
351
- // ['versions', index, 'edits', index, 'insert', index]
352
- const symbol = getAt(carrierPath.slice(0, 6), draft);
353
- if (!symbol)
354
- return;
355
- symbol.carriers = symbol.carriers.filter(c => idOf(c) !== featureId);
356
- // if the symbol has no more carriers, remove it
357
- if (symbol.carriers.length === 0) {
358
- // expect to find the parent edit at:
359
- // ['versions', index, 'edits', index]
360
- const inserts = getAt(carrierPath.slice(0, 5), draft);
361
- if (!inserts)
362
- return;
363
- inserts.splice(inserts.findIndex(s => s.id === symbol.id), 1);
364
- if (inserts.length === 0) {
365
- const edit = getAt(carrierPath.slice(0, 4), draft);
366
- if (!edit)
367
- return;
368
- if (!edit.insert?.length && !edit.delete?.length) {
369
- const edits = getAt(carrierPath.slice(0, 3), draft);
370
- if (!edits)
371
- return;
372
- edits.splice(edits.findIndex(e => e.id === edit.id), 1);
373
- }
374
- }
375
- }
376
- });
377
- }
378
- ];
379
- }
380
- }
381
- export class MergeEdits extends BasePlan {
382
- constructor(versionId, toMerge) {
383
- super();
384
- Object.defineProperty(this, "versionId", {
385
- enumerable: true,
386
- configurable: true,
387
- writable: true,
388
- value: versionId
389
- });
390
- Object.defineProperty(this, "toMerge", {
391
- enumerable: true,
392
- configurable: true,
393
- writable: true,
394
- value: toMerge
395
- });
396
- }
397
- guessMotivation(edit) {
398
- const view = this.view;
399
- if (!view)
400
- return 'correct-error';
401
- const inserts = (edit.insert || []);
402
- const deletes = view.getAll(edit.delete ?? []);
403
- const types = [
404
- inserts.filter(e => e.type === 'expression').map(e => e.expressionType),
405
- deletes.filter(e => e.type === 'expression').map(e => e.expressionType)
406
- ];
407
- if (arraysIdentical(types, [['SlowCrescendoOn', 'SlowCrescendoOff'], []])) {
408
- return 'additional-accent';
409
- }
410
- else if (arraysIdentical(types, [['ForzandoOn', 'ForzandoOff'], []])) {
411
- // TODO: check if the inserts are very close
412
- // and return 'short-dynamic-differentation'
413
- return 'additional-accent';
414
- }
415
- else if (types.every(t => t.length > 1) && arraysIdentical(types[0], types[1])) {
416
- return 'shift';
417
- }
418
- else if (types[0].length === 0 && types[1].length === 1) {
419
- return 'remove-redundancy';
420
- }
421
- if (inserts.length === 1 && deletes.length === 1) {
422
- const insertDim = view.dimensionOf(inserts[0])?.horizontal;
423
- const deleteDim = view.dimensionOf(deletes[0])?.horizontal;
424
- if (!insertDim || !deleteDim) {
425
- return 'correct-error';
426
- }
427
- if (Math.abs(insertDim.from - deleteDim.from) < 5) {
428
- const insertLength = Math.abs(insertDim.to - insertDim.from);
429
- const deleteLength = Math.abs(deleteDim.to - deleteDim.from);
430
- if (insertLength < deleteLength) {
431
- return 'shorten';
432
- }
433
- else {
434
- return 'prolong';
435
- }
436
- }
437
- }
438
- return 'correct-error';
439
- }
440
- build() {
441
- const view = this.view;
442
- if (!view)
443
- return [];
444
- const result = structuredClone(this.toMerge[0]);
445
- this.toMerge
446
- .slice(1)
447
- .forEach(edit => {
448
- if (result.insert) {
449
- result.insert.push(...(edit.insert || []));
450
- }
451
- else {
452
- result.insert = edit.insert;
453
- }
454
- if (result.delete) {
455
- result.delete.push(...(edit.delete || []));
456
- }
457
- else {
458
- result.delete = edit.delete;
459
- }
460
- });
461
- const edit = {
462
- ...result,
463
- id: v4(),
464
- motivation: this.guessMotivation(result),
465
- };
466
- return ([draft => {
467
- const version = draft.versions.find(v => v.id === this.versionId);
468
- if (!version)
469
- return;
470
- // remove all edits that were merged
471
- for (const e of this.toMerge) {
472
- const index = version.edits.findIndex(ve => ve.id === e.id);
473
- if (index !== -1)
474
- version.edits.splice(index, 1);
475
- }
476
- version.edits.push(edit);
477
- }]);
478
- }
479
- }
480
- export class SplitEdit extends BasePlan {
481
- constructor(versionId, toSplit) {
482
- super();
483
- Object.defineProperty(this, "versionId", {
484
- enumerable: true,
485
- configurable: true,
486
- writable: true,
487
- value: versionId
488
- });
489
- Object.defineProperty(this, "toSplit", {
490
- enumerable: true,
491
- configurable: true,
492
- writable: true,
493
- value: toSplit
494
- });
495
- }
496
- build() {
497
- const view = this.view;
498
- if (!view)
499
- return [];
500
- const result = [];
501
- for (const insert of this.toSplit.insert ?? []) {
502
- result.push({
503
- type: 'edit',
504
- id: v4(),
505
- insert: [insert]
506
- });
507
- }
508
- for (const remove of this.toSplit.delete ?? []) {
509
- result.push({
510
- type: 'edit',
511
- id: v4(),
512
- delete: [remove]
513
- });
514
- }
515
- return ([draft => {
516
- const version = draft.versions.find(v => v.id === this.versionId);
517
- if (!version)
518
- return;
519
- // remove the edit that is split
520
- const index = version.edits.findIndex(ve => ve.id === this.toSplit.id);
521
- if (index !== -1)
522
- version.edits.splice(index, 1);
523
- version.edits.push(...result);
524
- }]);
525
- }
526
- }
527
- /*
528
- type LazyDimension = Partial<{ from: number, to: number }>;
529
-
530
- type LazyArea = {
531
- horizontal: LazyDimension;
532
- vertical: LazyDimension;
533
- }
534
-
535
- const overlaps = (a: LazyArea, b: LazyArea): boolean => {
536
- const overlapsDimension = (a: LazyDimension, b: LazyDimension): boolean =>
537
- (a.from ?? 0) < (b.to ?? Infinity) && (b.from ?? 0) < (a.to ?? Infinity);
538
-
539
- // console.log('do the dimensions overlap?', a, b)
540
-
541
- return overlapsDimension(a.horizontal, b.horizontal);
542
- }
543
- */
544
- const arraysIdentical = (a, b) => {
545
- let i = a.length;
546
- if (i != b.length)
547
- return false;
548
- while (i--) {
549
- if (Array.isArray(a[i]) && Array.isArray(b[i])) {
550
- return arraysIdentical(a[i], b[i]);
551
- }
552
- if (a[i] !== b[i])
553
- return false;
554
- }
555
- return true;
556
- };