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 +4 -0
- package/lib/RollCopy.js +33 -0
- package/lib/Symbol.d.ts +2 -0
- package/lib/Symbol.js +1 -0
- package/lib/editionOps.d.ts +76 -0
- package/lib/editionOps.js +315 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/schema.json +2 -2
- package/lib/validate.js +1 -2
- package/package.json +1 -1
- package/lib/Plan.d.ts +0 -190
- package/lib/Plan.js +0 -556
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
package/lib/index.js
CHANGED
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-
|
|
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-
|
|
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
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
|
-
};
|