linked-rolls 0.4.1 → 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,7 +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: AnySymbol) => symbol is Note | Expression;
22
+ export declare const isPerforation: (symbol: object | undefined) => symbol is AnyPerforation;
23
23
  /**
24
24
  * A perforation is a symbol that is typically encoded as a single punched
25
25
  * hole or a group of punched holes in the physical carrier.
@@ -142,4 +142,5 @@ export interface Text extends Symbol<'text'> {
142
142
  * Notes and expressions are perforations; texts are carried by writings.
143
143
  */
144
144
  export type AnySymbol = Note | Expression | Text;
145
+ export type AnyPerforation = Note | Expression;
145
146
  export {};
package/lib/Symbol.js CHANGED
@@ -3,7 +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.type !== 'text';
6
+ export const isPerforation = (symbol) => symbol !== undefined && 'type' in symbol && (symbol.type === 'note' || symbol.type === 'expression');
7
7
  export const placementRelations = ['alignedWith', 'before', 'after'];
8
8
  /**
9
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-5670-6034-src_Symbol.ts-0-6242400334313,\"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-5670-6034-src_Symbol.ts-0-6242400334313%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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.4.1",
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,198 +0,0 @@
1
- import { Draft } from "immer";
2
- import { EditionView } from "./EditionView";
3
- import { Edition } from "./Edition";
4
- import { AnySymbol } from "./Symbol";
5
- import { CollationTolerance } from "./Collation";
6
- import { Edit } from "./Edit";
7
- import { RollCopy } from "./RollCopy";
8
- export type EditionOp = (d: Draft<Edition>) => void;
9
- export interface Plan {
10
- build(): EditionOp[];
11
- setView: (view: EditionView) => void;
12
- }
13
- export declare const isPlan: (obj: any) => obj is Plan;
14
- export declare abstract class BasePlan implements Plan {
15
- protected view: EditionView | null;
16
- setView: (view: EditionView) => void;
17
- abstract build(): EditionOp[];
18
- }
19
- /**
20
- * Walks through the versions. If it finds a symbol that is still
21
- * part of the tradition (i.e. it is included in the current snapshot)
22
- * and is equivalent with the given symbol, it will add the feature
23
- * carrying the symbol to the collated symbol. Otherwise, the
24
- * given symbol will be added to the current version's insertions.
25
- *
26
- * All symbols of the tradition that are not included in the given
27
- * symbols are considered to be deleted.
28
- *
29
- * @param creation
30
- * @param symbols
31
- * @returns
32
- */
33
- export declare class ConnectVersions extends BasePlan {
34
- private childId;
35
- private parentId;
36
- private tolerance;
37
- constructor(childId: string, parentId: string, tolerance?: CollationTolerance);
38
- build(): EditionOp[];
39
- }
40
- /**
41
- * Walks through the versions. If it finds a symbol that is still
42
- * part of the tradition (i.e. it is included in the current snapshot)
43
- * and is equivalent with the given symbol, it will add the feature
44
- * carrying the symbol to the collated symbol. Otherwise, the
45
- * given symbol will be added to the current version's insertions.
46
- *
47
- * All symbols of the tradition that are not included in the given
48
- * symbols are considered to be deleted.
49
- *
50
- * @param creation
51
- * @param symbols
52
- * @returns
53
- */
54
- export declare class CreateVersion extends BasePlan {
55
- private siglum;
56
- private copy;
57
- constructor(siglum: string, copy: RollCopy);
58
- build(): EditionOp[];
59
- }
60
- /**
61
- * Creates a new version based on the given version
62
- * calculating the effect of a cover on existing symbols.
63
-
64
- export class CoverPerforation extends BasePlan {
65
- constructor(
66
- private copyId: string,
67
- private versionId: string,
68
- private coverDimension: { horizontal: HorizontalSpan, vertical: VerticalSpan },
69
- private material: string | undefined = undefined,
70
- ) {
71
- super()
72
- }
73
-
74
- build(): EditionOp[] {
75
- const view = this.view
76
- if (!view) return []
77
-
78
- return [
79
- (draft: Draft<Edition>): void => {
80
- const version = draft.versions.find(v => v.id === this.versionId)
81
- const copy = draft.copies.find(c => c.id === this.copyId)
82
- if (!version || !copy) return
83
-
84
- // find perforations in the snapshot that
85
- // overlap with the cover
86
- const overlappingSymbols =
87
- view.snapshot(this.versionId)
88
- .filter(symbol => {
89
- const dimension = view.dimensionOf(symbol)
90
- if (!dimension) return false
91
-
92
- return (
93
- (symbol.type === 'note' || symbol.type === 'expression') &&
94
- overlaps(dimension, this.coverDimension)
95
- )
96
- })
97
-
98
- if (!overlappingSymbols.length) return undefined
99
-
100
- const deepClone = JSON.parse(JSON.stringify(overlappingSymbols)) as AnySymbol[]
101
-
102
- for (const symbol of deepClone) {
103
- //symbol.id = v4();
104
- const dimension = structuredClone(view.dimensionOf(symbol))
105
- if (!dimension) continue
106
-
107
- // check if the cover partially covers the beginning
108
- if (dimension.horizontal.from >= this.coverDimension.horizontal.from &&
109
- dimension.horizontal.from <= this.coverDimension.horizontal.to
110
- ) {
111
- dimension.horizontal.from = this.coverDimension.horizontal.to
112
- }
113
-
114
- // check if the cover partially covers the ending
115
- if (dimension.horizontal.to >= this.coverDimension.horizontal.from &&
116
- dimension.horizontal.to <= this.coverDimension.horizontal.to
117
- ) {
118
- dimension.horizontal.to = this.coverDimension.horizontal.from
119
- }
120
-
121
- const newFeature: RollFeature = {
122
- ...dimension,
123
- id: v4(),
124
- }
125
-
126
- copy.features.push(newFeature)
127
- symbol.id = `symbol-${v4().slice(0, 8)}`
128
- symbol.carriers = [assignReference(newFeature.id)]
129
- }
130
-
131
- const coverId = `cover-${v4().slice(0, 8)}`
132
- copy.features.push({
133
- ...this.coverDimension,
134
- id: coverId,
135
- })
136
-
137
- const edit: Edit = {
138
- type: 'edit',
139
- id: v4(),
140
- delete: overlappingSymbols.map(s => s.id),
141
- insert: deepClone,
142
- intentionOf: [{
143
- type: 'cover',
144
- id: v4(),
145
- note: this.material,
146
- carriers: [assignReference(coverId)],
147
- }]
148
- }
149
-
150
- draft.versions.push({
151
- edits: [edit],
152
- id: v4(),
153
- basedOn: assignReference(version.id),
154
- siglum: version.siglum + ' rev',
155
- type: 'authorised-revision',
156
- motivations: [
157
- {
158
- type: 'motivation',
159
- note: 'Stanzfehler korrigiert',
160
- id: v4(),
161
- }
162
- ],
163
- })
164
- }]
165
- }
166
- }
167
- */
168
- export declare class DetachVersion extends BasePlan {
169
- private versionId;
170
- constructor(versionId: string);
171
- build(): EditionOp[];
172
- }
173
- export declare class RemoveFeature extends BasePlan {
174
- private copyID;
175
- private featureIDs;
176
- constructor(copyID: string, featureIDs: string[]);
177
- build(): EditionOp[];
178
- }
179
- /** The symbols of the versions that no other copy carries. */
180
- export declare const symbolsCarriedOnlyBy: (edition: Edition, copyId: string) => AnySymbol[];
181
- /**
182
- * Takes the copy out of the edition together with the symbols only it
183
- * carries, and with every reference the versions made to those symbols.
184
- */
185
- export declare const removeCopy: (copyId: string) => EditionOp;
186
- export declare class MergeEdits extends BasePlan {
187
- private versionId;
188
- private toMerge;
189
- constructor(versionId: string, toMerge: Edit[]);
190
- private guessMotivation;
191
- build(): EditionOp[];
192
- }
193
- export declare class SplitEdit extends BasePlan {
194
- private versionId;
195
- private toSplit;
196
- constructor(versionId: string, toSplit: Edit);
197
- build(): EditionOp[];
198
- }
package/lib/Plan.js DELETED
@@ -1,611 +0,0 @@
1
- import { getAt } from "./EditionView";
2
- import { isPerforation, placementRelations } from "./Symbol";
3
- import { v4 } from "uuid";
4
- import { asSymbols } from "./RollCopy";
5
- import { assignReference, idOf } from "./Assumption";
6
- export const isPlan = (obj) => {
7
- return obj && typeof obj.build === 'function' && typeof obj.setView === 'function';
8
- };
9
- export class BasePlan {
10
- constructor() {
11
- Object.defineProperty(this, "view", {
12
- enumerable: true,
13
- configurable: true,
14
- writable: true,
15
- value: null
16
- });
17
- Object.defineProperty(this, "setView", {
18
- enumerable: true,
19
- configurable: true,
20
- writable: true,
21
- value: (view) => {
22
- this.view = view;
23
- }
24
- });
25
- }
26
- }
27
- /**
28
- * Walks through the versions. If it finds a symbol that is still
29
- * part of the tradition (i.e. it is included in the current snapshot)
30
- * and is equivalent with the given symbol, it will add the feature
31
- * carrying the symbol to the collated symbol. Otherwise, the
32
- * given symbol will be added to the current version's insertions.
33
- *
34
- * All symbols of the tradition that are not included in the given
35
- * symbols are considered to be deleted.
36
- *
37
- * @param creation
38
- * @param symbols
39
- * @returns
40
- */
41
- export class ConnectVersions extends BasePlan {
42
- constructor(childId, parentId, tolerance = {
43
- toleranceEnd: 5,
44
- toleranceStart: 5
45
- }) {
46
- super();
47
- Object.defineProperty(this, "childId", {
48
- enumerable: true,
49
- configurable: true,
50
- writable: true,
51
- value: childId
52
- });
53
- Object.defineProperty(this, "parentId", {
54
- enumerable: true,
55
- configurable: true,
56
- writable: true,
57
- value: parentId
58
- });
59
- Object.defineProperty(this, "tolerance", {
60
- enumerable: true,
61
- configurable: true,
62
- writable: true,
63
- value: tolerance
64
- });
65
- }
66
- build() {
67
- if (!this.view)
68
- return [];
69
- const view = this.view;
70
- const result = [];
71
- const parentSnapshot = view.snapshot(this.parentId);
72
- const childSnapshot = view.snapshot(this.childId);
73
- const treatedSymbols = [];
74
- // can the symbol be collated with any of the
75
- // symbols of the tradition, i.e. the ones
76
- // included in the current snapshot?
77
- const insertions = [...childSnapshot];
78
- for (const symbol of childSnapshot) {
79
- parentSnapshot
80
- .filter(toCompare => view.isCollatable(symbol, toCompare, this.tolerance))
81
- .forEach(parentSymbol => {
82
- result.push(draft => {
83
- const symbolPath = view.getPath(parentSymbol.id);
84
- if (!symbolPath)
85
- return;
86
- const correspSymbol = getAt(symbolPath, draft);
87
- if (!correspSymbol)
88
- return;
89
- correspSymbol.carriers.push(...symbol.carriers);
90
- });
91
- insertions.splice(insertions.indexOf(symbol), 1);
92
- treatedSymbols.push(parentSymbol);
93
- });
94
- }
95
- const edits = insertions.map((symbol) => {
96
- return {
97
- type: 'edit',
98
- id: v4(),
99
- insert: [symbol],
100
- delete: [],
101
- };
102
- });
103
- const deletions = parentSnapshot.filter(sym => {
104
- return !treatedSymbols.includes(sym);
105
- });
106
- for (const symbol of deletions) {
107
- edits.push({
108
- type: 'edit',
109
- id: v4(),
110
- insert: [],
111
- delete: [symbol.id],
112
- });
113
- }
114
- result.push(draft => {
115
- const v = draft.versions.find(v => v.id === this.childId);
116
- if (!v)
117
- return;
118
- v.edits = edits;
119
- v.basedOn = assignReference(this.parentId);
120
- });
121
- return result;
122
- }
123
- }
124
- /**
125
- * Walks through the versions. If it finds a symbol that is still
126
- * part of the tradition (i.e. it is included in the current snapshot)
127
- * and is equivalent with the given symbol, it will add the feature
128
- * carrying the symbol to the collated symbol. Otherwise, the
129
- * given symbol will be added to the current version's insertions.
130
- *
131
- * All symbols of the tradition that are not included in the given
132
- * symbols are considered to be deleted.
133
- *
134
- * @param creation
135
- * @param symbols
136
- * @returns
137
- */
138
- export class CreateVersion extends BasePlan {
139
- constructor(siglum, copy) {
140
- super();
141
- Object.defineProperty(this, "siglum", {
142
- enumerable: true,
143
- configurable: true,
144
- writable: true,
145
- value: siglum
146
- });
147
- Object.defineProperty(this, "copy", {
148
- enumerable: true,
149
- configurable: true,
150
- writable: true,
151
- value: copy
152
- });
153
- }
154
- build() {
155
- const view = this.view;
156
- if (!view)
157
- return [];
158
- const result = [];
159
- result.push(draft => {
160
- draft.copies.push(this.copy);
161
- const newVersion = {
162
- type: 'Version',
163
- siglum: this.siglum,
164
- id: v4(),
165
- edits: asSymbols(this.copy.features).map((symbol) => {
166
- return {
167
- type: 'edit',
168
- id: v4(),
169
- insert: [symbol],
170
- delete: [],
171
- };
172
- }),
173
- versionType: 'edition',
174
- motivations: []
175
- };
176
- draft.versions.push(newVersion);
177
- });
178
- return result;
179
- }
180
- }
181
- /**
182
- * Creates a new version based on the given version
183
- * calculating the effect of a cover on existing symbols.
184
-
185
- export class CoverPerforation extends BasePlan {
186
- constructor(
187
- private copyId: string,
188
- private versionId: string,
189
- private coverDimension: { horizontal: HorizontalSpan, vertical: VerticalSpan },
190
- private material: string | undefined = undefined,
191
- ) {
192
- super()
193
- }
194
-
195
- build(): EditionOp[] {
196
- const view = this.view
197
- if (!view) return []
198
-
199
- return [
200
- (draft: Draft<Edition>): void => {
201
- const version = draft.versions.find(v => v.id === this.versionId)
202
- const copy = draft.copies.find(c => c.id === this.copyId)
203
- if (!version || !copy) return
204
-
205
- // find perforations in the snapshot that
206
- // overlap with the cover
207
- const overlappingSymbols =
208
- view.snapshot(this.versionId)
209
- .filter(symbol => {
210
- const dimension = view.dimensionOf(symbol)
211
- if (!dimension) return false
212
-
213
- return (
214
- (symbol.type === 'note' || symbol.type === 'expression') &&
215
- overlaps(dimension, this.coverDimension)
216
- )
217
- })
218
-
219
- if (!overlappingSymbols.length) return undefined
220
-
221
- const deepClone = JSON.parse(JSON.stringify(overlappingSymbols)) as AnySymbol[]
222
-
223
- for (const symbol of deepClone) {
224
- //symbol.id = v4();
225
- const dimension = structuredClone(view.dimensionOf(symbol))
226
- if (!dimension) continue
227
-
228
- // check if the cover partially covers the beginning
229
- if (dimension.horizontal.from >= this.coverDimension.horizontal.from &&
230
- dimension.horizontal.from <= this.coverDimension.horizontal.to
231
- ) {
232
- dimension.horizontal.from = this.coverDimension.horizontal.to
233
- }
234
-
235
- // check if the cover partially covers the ending
236
- if (dimension.horizontal.to >= this.coverDimension.horizontal.from &&
237
- dimension.horizontal.to <= this.coverDimension.horizontal.to
238
- ) {
239
- dimension.horizontal.to = this.coverDimension.horizontal.from
240
- }
241
-
242
- const newFeature: RollFeature = {
243
- ...dimension,
244
- id: v4(),
245
- }
246
-
247
- copy.features.push(newFeature)
248
- symbol.id = `symbol-${v4().slice(0, 8)}`
249
- symbol.carriers = [assignReference(newFeature.id)]
250
- }
251
-
252
- const coverId = `cover-${v4().slice(0, 8)}`
253
- copy.features.push({
254
- ...this.coverDimension,
255
- id: coverId,
256
- })
257
-
258
- const edit: Edit = {
259
- type: 'edit',
260
- id: v4(),
261
- delete: overlappingSymbols.map(s => s.id),
262
- insert: deepClone,
263
- intentionOf: [{
264
- type: 'cover',
265
- id: v4(),
266
- note: this.material,
267
- carriers: [assignReference(coverId)],
268
- }]
269
- }
270
-
271
- draft.versions.push({
272
- edits: [edit],
273
- id: v4(),
274
- basedOn: assignReference(version.id),
275
- siglum: version.siglum + ' rev',
276
- type: 'authorised-revision',
277
- motivations: [
278
- {
279
- type: 'motivation',
280
- note: 'Stanzfehler korrigiert',
281
- id: v4(),
282
- }
283
- ],
284
- })
285
- }]
286
- }
287
- }
288
- */
289
- export class DetachVersion extends BasePlan {
290
- constructor(versionId) {
291
- super();
292
- Object.defineProperty(this, "versionId", {
293
- enumerable: true,
294
- configurable: true,
295
- writable: true,
296
- value: versionId
297
- });
298
- }
299
- build() {
300
- if (!this.view)
301
- return [];
302
- const snapshot = this.view.snapshot(this.versionId);
303
- const newEdits = snapshot.map((symbol) => ({
304
- type: 'edit',
305
- id: v4(),
306
- insert: [symbol],
307
- delete: [],
308
- }));
309
- return [draft => {
310
- const v = draft.versions.find(ver => ver.id === this.versionId);
311
- if (!v)
312
- return;
313
- v.edits = newEdits;
314
- delete v.basedOn;
315
- v.motivations = [];
316
- }];
317
- }
318
- }
319
- export class RemoveFeature extends BasePlan {
320
- constructor(copyID, featureIDs) {
321
- super();
322
- Object.defineProperty(this, "copyID", {
323
- enumerable: true,
324
- configurable: true,
325
- writable: true,
326
- value: copyID
327
- });
328
- Object.defineProperty(this, "featureIDs", {
329
- enumerable: true,
330
- configurable: true,
331
- writable: true,
332
- value: featureIDs
333
- });
334
- }
335
- build() {
336
- const view = this.view;
337
- if (!view)
338
- return [];
339
- return [
340
- (draft) => {
341
- const copy = draft.copies.find(c => c.id === this.copyID);
342
- if (!copy)
343
- return;
344
- copy.features = copy.features.filter(f => !this.featureIDs.includes(f.id));
345
- this.featureIDs.forEach(featureId => {
346
- // expect a path of format:
347
- // ['versions', index, 'edits', index, 'insert', index, 'carriers', index, 'id']
348
- const carrierPath = view.linksTo(featureId).at(0);
349
- if (!carrierPath)
350
- return;
351
- // expect to find the parent symbol at:
352
- // ['versions', index, 'edits', index, 'insert', index]
353
- const symbol = getAt(carrierPath.slice(0, 6), draft);
354
- if (!symbol)
355
- return;
356
- symbol.carriers = symbol.carriers.filter(c => idOf(c) !== featureId);
357
- // if the symbol has no more carriers, remove it
358
- if (symbol.carriers.length === 0) {
359
- // expect to find the parent edit at:
360
- // ['versions', index, 'edits', index]
361
- const inserts = getAt(carrierPath.slice(0, 5), draft);
362
- if (!inserts)
363
- return;
364
- inserts.splice(inserts.findIndex(s => s.id === symbol.id), 1);
365
- if (inserts.length === 0) {
366
- const edit = getAt(carrierPath.slice(0, 4), draft);
367
- if (!edit)
368
- return;
369
- if (!edit.insert?.length && !edit.delete?.length) {
370
- const edits = getAt(carrierPath.slice(0, 3), draft);
371
- if (!edits)
372
- return;
373
- edits.splice(edits.findIndex(e => e.id === edit.id), 1);
374
- }
375
- }
376
- }
377
- });
378
- }
379
- ];
380
- }
381
- }
382
- const featureIdsOf = (copy) => new Set(copy.features.map(feature => feature.id));
383
- const insertedIn = (versions) => versions.flatMap(version => version.edits).flatMap(edit => edit.insert ?? []);
384
- /**
385
- * A symbol every carrier of which lies on the copy loses its evidence
386
- * with the copy. A symbol without carriers, such as a label, stands
387
- * on its own.
388
- */
389
- const carriedOnlyOn = (features) => (symbol) => symbol.carriers.length > 0 && symbol.carriers.every(carrier => features.has(idOf(carrier)));
390
- /** The symbols of the versions that no other copy carries. */
391
- export const symbolsCarriedOnlyBy = (edition, copyId) => {
392
- const copy = edition.copies.find(c => c.id === copyId);
393
- return copy ? insertedIn(edition.versions).filter(carriedOnlyOn(featureIdsOf(copy))) : [];
394
- };
395
- const references = [...placementRelations, 'pairedWith'];
396
- const forgetPerforations = (perforation, dropped) => references
397
- .filter(relation => {
398
- const reference = perforation[relation];
399
- return reference && dropped.has(idOf(reference));
400
- })
401
- .forEach(relation => { delete perforation[relation]; });
402
- const forgetFeatures = (symbol, features, dropped) => {
403
- symbol.carriers = symbol.carriers.filter(carrier => !features.has(idOf(carrier)));
404
- if (isPerforation(symbol))
405
- forgetPerforations(symbol, dropped);
406
- };
407
- const forgetCopy = (edit, features, dropped) => {
408
- if (edit.insert) {
409
- edit.insert = edit.insert.filter(symbol => !dropped.has(symbol.id));
410
- edit.insert.forEach(symbol => forgetFeatures(symbol, features, dropped));
411
- }
412
- if (edit.delete) {
413
- edit.delete = edit.delete.filter(id => !dropped.has(id));
414
- }
415
- };
416
- const isEmpty = (edit) => !edit.insert?.length && !edit.delete?.length;
417
- /** Drops the edits the removal has emptied and leaves those that were empty before alone. */
418
- const forgetCopyIn = (version, features, dropped) => {
419
- const emptyBefore = new Set(version.edits.filter(isEmpty).map(edit => edit.id));
420
- version.edits.forEach(edit => forgetCopy(edit, features, dropped));
421
- version.edits = version.edits.filter(edit => !isEmpty(edit) || emptyBefore.has(edit.id));
422
- };
423
- /**
424
- * Takes the copy out of the edition together with the symbols only it
425
- * carries, and with every reference the versions made to those symbols.
426
- */
427
- export const removeCopy = (copyId) => draft => {
428
- const copy = draft.copies.find(c => c.id === copyId);
429
- if (!copy)
430
- return;
431
- const features = featureIdsOf(copy);
432
- const dropped = new Set(insertedIn(draft.versions).filter(carriedOnlyOn(features)).map(symbol => symbol.id));
433
- draft.copies = draft.copies.filter(c => c.id !== copyId);
434
- draft.versions.forEach(version => forgetCopyIn(version, features, dropped));
435
- };
436
- export class MergeEdits extends BasePlan {
437
- constructor(versionId, toMerge) {
438
- super();
439
- Object.defineProperty(this, "versionId", {
440
- enumerable: true,
441
- configurable: true,
442
- writable: true,
443
- value: versionId
444
- });
445
- Object.defineProperty(this, "toMerge", {
446
- enumerable: true,
447
- configurable: true,
448
- writable: true,
449
- value: toMerge
450
- });
451
- }
452
- guessMotivation(edit) {
453
- const view = this.view;
454
- if (!view)
455
- return 'correct-error';
456
- const inserts = (edit.insert || []);
457
- const deletes = view.getAll(edit.delete ?? []);
458
- const types = [
459
- inserts.filter(e => e.type === 'expression').map(e => e.expressionType),
460
- deletes.filter(e => e.type === 'expression').map(e => e.expressionType)
461
- ];
462
- if (arraysIdentical(types, [['SlowCrescendoOn', 'SlowCrescendoOff'], []])) {
463
- return 'additional-accent';
464
- }
465
- else if (arraysIdentical(types, [['ForzandoOn', 'ForzandoOff'], []])) {
466
- // TODO: check if the inserts are very close
467
- // and return 'short-dynamic-differentation'
468
- return 'additional-accent';
469
- }
470
- else if (types.every(t => t.length > 1) && arraysIdentical(types[0], types[1])) {
471
- return 'shift';
472
- }
473
- else if (types[0].length === 0 && types[1].length === 1) {
474
- return 'remove-redundancy';
475
- }
476
- if (inserts.length === 1 && deletes.length === 1) {
477
- const insertDim = view.dimensionOf(inserts[0])?.horizontal;
478
- const deleteDim = view.dimensionOf(deletes[0])?.horizontal;
479
- if (!insertDim || !deleteDim) {
480
- return 'correct-error';
481
- }
482
- if (Math.abs(insertDim.from - deleteDim.from) < 5) {
483
- const insertLength = Math.abs(insertDim.to - insertDim.from);
484
- const deleteLength = Math.abs(deleteDim.to - deleteDim.from);
485
- if (insertLength < deleteLength) {
486
- return 'shorten';
487
- }
488
- else {
489
- return 'prolong';
490
- }
491
- }
492
- }
493
- return 'correct-error';
494
- }
495
- build() {
496
- const view = this.view;
497
- if (!view)
498
- return [];
499
- const result = structuredClone(this.toMerge[0]);
500
- this.toMerge
501
- .slice(1)
502
- .forEach(edit => {
503
- if (result.insert) {
504
- result.insert.push(...(edit.insert || []));
505
- }
506
- else {
507
- result.insert = edit.insert;
508
- }
509
- if (result.delete) {
510
- result.delete.push(...(edit.delete || []));
511
- }
512
- else {
513
- result.delete = edit.delete;
514
- }
515
- });
516
- const edit = {
517
- ...result,
518
- id: v4(),
519
- motivation: this.guessMotivation(result),
520
- };
521
- return ([draft => {
522
- const version = draft.versions.find(v => v.id === this.versionId);
523
- if (!version)
524
- return;
525
- // remove all edits that were merged
526
- for (const e of this.toMerge) {
527
- const index = version.edits.findIndex(ve => ve.id === e.id);
528
- if (index !== -1)
529
- version.edits.splice(index, 1);
530
- }
531
- version.edits.push(edit);
532
- }]);
533
- }
534
- }
535
- export class SplitEdit extends BasePlan {
536
- constructor(versionId, toSplit) {
537
- super();
538
- Object.defineProperty(this, "versionId", {
539
- enumerable: true,
540
- configurable: true,
541
- writable: true,
542
- value: versionId
543
- });
544
- Object.defineProperty(this, "toSplit", {
545
- enumerable: true,
546
- configurable: true,
547
- writable: true,
548
- value: toSplit
549
- });
550
- }
551
- build() {
552
- const view = this.view;
553
- if (!view)
554
- return [];
555
- const result = [];
556
- for (const insert of this.toSplit.insert ?? []) {
557
- result.push({
558
- type: 'edit',
559
- id: v4(),
560
- insert: [insert]
561
- });
562
- }
563
- for (const remove of this.toSplit.delete ?? []) {
564
- result.push({
565
- type: 'edit',
566
- id: v4(),
567
- delete: [remove]
568
- });
569
- }
570
- return ([draft => {
571
- const version = draft.versions.find(v => v.id === this.versionId);
572
- if (!version)
573
- return;
574
- // remove the edit that is split
575
- const index = version.edits.findIndex(ve => ve.id === this.toSplit.id);
576
- if (index !== -1)
577
- version.edits.splice(index, 1);
578
- version.edits.push(...result);
579
- }]);
580
- }
581
- }
582
- /*
583
- type LazyDimension = Partial<{ from: number, to: number }>;
584
-
585
- type LazyArea = {
586
- horizontal: LazyDimension;
587
- vertical: LazyDimension;
588
- }
589
-
590
- const overlaps = (a: LazyArea, b: LazyArea): boolean => {
591
- const overlapsDimension = (a: LazyDimension, b: LazyDimension): boolean =>
592
- (a.from ?? 0) < (b.to ?? Infinity) && (b.from ?? 0) < (a.to ?? Infinity);
593
-
594
- // console.log('do the dimensions overlap?', a, b)
595
-
596
- return overlapsDimension(a.horizontal, b.horizontal);
597
- }
598
- */
599
- const arraysIdentical = (a, b) => {
600
- let i = a.length;
601
- if (i != b.length)
602
- return false;
603
- while (i--) {
604
- if (Array.isArray(a[i]) && Array.isArray(b[i])) {
605
- return arraysIdentical(a[i], b[i]);
606
- }
607
- if (a[i] !== b[i])
608
- return false;
609
- }
610
- return true;
611
- };