linked-rolls 0.56.0 → 0.58.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/README.md +54 -361
- package/lib/{constraints.d.ts → analysis/constraints.d.ts} +1 -1
- package/lib/{constraints.js → analysis/constraints.js} +9 -8
- package/lib/analysis/ownPaper.d.ts +26 -0
- package/lib/analysis/ownPaper.js +41 -0
- package/lib/{reservations.d.ts → analysis/reservations.d.ts} +3 -3
- package/lib/{reservations.js → analysis/reservations.js} +3 -3
- package/lib/{sigla.d.ts → analysis/sigla.d.ts} +2 -2
- package/lib/{sigla.js → analysis/sigla.js} +4 -4
- package/lib/{witnesses.d.ts → analysis/witnesses.d.ts} +2 -2
- package/lib/{witnesses.js → analysis/witnesses.js} +4 -4
- package/lib/{Collation.d.ts → collation/Collation.d.ts} +4 -4
- package/lib/{Collation.js → collation/Collation.js} +4 -4
- package/lib/{alignment.d.ts → collation/alignment.d.ts} +4 -4
- package/lib/{alignment.js → collation/alignment.js} +10 -16
- package/lib/{scatter.d.ts → collation/scatter.d.ts} +4 -4
- package/lib/{scatter.js → collation/scatter.js} +6 -6
- package/lib/{statistics.d.ts → collation/statistics.d.ts} +1 -1
- package/lib/{statistics.js → collation/statistics.js} +2 -2
- package/lib/{substitution.d.ts → collation/substitution.d.ts} +9 -24
- package/lib/{substitution.js → collation/substitution.js} +13 -37
- package/lib/emulation/Emulation.d.ts +64 -0
- package/lib/emulation/Emulation.js +282 -0
- package/lib/emulation/negotiation.d.ts +14 -0
- package/lib/emulation/negotiation.js +20 -0
- package/lib/index.d.ts +43 -35
- package/lib/index.js +36 -35
- package/lib/{asJsonLd.d.ts → io/asJsonLd.d.ts} +1 -1
- package/lib/{asJsonLd.js → io/asJsonLd.js} +22 -51
- package/lib/{context.d.ts → io/context.d.ts} +4 -4
- package/lib/{context.js → io/context.js} +4 -4
- package/lib/{importJsonLd.d.ts → io/importJsonLd.d.ts} +1 -1
- package/lib/io/importJsonLd.js +128 -0
- package/lib/{migrate.d.ts → io/migrate.d.ts} +9 -0
- package/lib/{migrate.js → io/migrate.js} +97 -54
- package/lib/{Agent.d.ts → model/Agent.d.ts} +1 -1
- package/lib/{Assumption.d.ts → model/Assumption.d.ts} +1 -1
- package/lib/{ConditionState.d.ts → model/ConditionState.d.ts} +1 -2
- package/lib/{Edit.d.ts → model/Edit.d.ts} +1 -1
- package/lib/{Edition.d.ts → model/Edition.d.ts} +2 -2
- package/lib/{Feature.d.ts → model/Feature.d.ts} +3 -1
- package/lib/{Feature.js → model/Feature.js} +2 -0
- package/lib/{FeatureSource.d.ts → model/FeatureSource.d.ts} +1 -1
- package/lib/{Perforator.d.ts → model/Perforator.d.ts} +1 -1
- package/lib/{RollCopy.d.ts → model/RollCopy.d.ts} +5 -12
- package/lib/{RollCopy.js → model/RollCopy.js} +3 -5
- package/lib/{Symbol.d.ts → model/Symbol.d.ts} +1 -1
- package/lib/{Version.d.ts → model/Version.d.ts} +11 -3
- package/lib/{Version.js → model/Version.js} +24 -1
- package/lib/{vocabulary.js → model/vocabulary.js} +2 -2
- package/lib/ops/beliefs.d.ts +9 -0
- package/lib/ops/beliefs.js +31 -0
- package/lib/ops/collation.d.ts +62 -0
- package/lib/ops/collation.js +228 -0
- package/lib/ops/commands.d.ts +9 -0
- package/lib/ops/commands.js +29 -0
- package/lib/ops/copies.d.ts +74 -0
- package/lib/ops/copies.js +135 -0
- package/lib/ops/draft.d.ts +59 -0
- package/lib/ops/draft.js +81 -0
- package/lib/ops/edits.d.ts +10 -0
- package/lib/ops/edits.js +82 -0
- package/lib/ops/features.d.ts +84 -0
- package/lib/ops/features.js +296 -0
- package/lib/ops/forget.d.ts +31 -0
- package/lib/ops/forget.js +110 -0
- package/lib/ops/immutable.d.ts +16 -0
- package/lib/ops/immutable.js +46 -0
- package/lib/ops/index.d.ts +13 -0
- package/lib/ops/index.js +7 -0
- package/lib/ops/versions.d.ts +33 -0
- package/lib/ops/versions.js +95 -0
- package/lib/readers/AtonParser.js +5 -7
- package/lib/readers/phillipsEroll.d.ts +3 -3
- package/lib/readers/phillipsEroll.js +4 -5
- package/lib/readers/spencerBar.d.ts +2 -2
- package/lib/readers/spencerBar.js +2 -3
- package/lib/readers/stanfordAton.d.ts +3 -3
- package/lib/readers/stanfordAton.js +2 -4
- package/lib/schema.json +11 -151
- package/lib/spec/context.json +2 -12
- package/lib/{ReproducingSystem.d.ts → systems/ReproducingSystem.d.ts} +3 -3
- package/lib/{TrackerBar.d.ts → systems/TrackerBar.d.ts} +48 -8
- package/lib/{TrackerBar.js → systems/TrackerBar.js} +11 -2
- package/lib/systems/index.d.ts +8 -2
- package/lib/systems/index.js +7 -1
- package/lib/systems/welte.d.ts +100 -0
- package/lib/systems/welte.js +119 -0
- package/lib/systems/welteLicensee/bar.d.ts +1 -1
- package/lib/systems/welteLicensee/bar.js +5 -2
- package/lib/systems/welteLicensee/system.d.ts +1 -1
- package/lib/systems/welteLicensee/system.js +1 -1
- package/lib/systems/welteT100/bar.d.ts +27 -1
- package/lib/systems/welteT100/bar.js +44 -2
- package/lib/systems/welteT100/system.d.ts +6 -39
- package/lib/systems/welteT100/system.js +19 -128
- package/lib/systems/welteT98/bar.d.ts +1 -1
- package/lib/systems/welteT98/bar.js +18 -2
- package/lib/systems/welteT98/system.d.ts +20 -38
- package/lib/systems/welteT98/system.js +20 -121
- package/lib/validate.d.ts +1 -1
- package/lib/{EditionView.d.ts → view/EditionView.d.ts} +38 -48
- package/lib/{EditionView.js → view/EditionView.js} +90 -96
- package/package.json +7 -2
- package/lib/Emulation.d.ts +0 -36
- package/lib/Emulation.js +0 -250
- package/lib/editionOps.d.ts +0 -260
- package/lib/editionOps.js +0 -1081
- package/lib/importJsonLd.js +0 -62
- /package/lib/{Agent.js → model/Agent.js} +0 -0
- /package/lib/{Assumption.js → model/Assumption.js} +0 -0
- /package/lib/{ConditionState.js → model/ConditionState.js} +0 -0
- /package/lib/{Edit.js → model/Edit.js} +0 -0
- /package/lib/{Edition.js → model/Edition.js} +0 -0
- /package/lib/{FeatureSource.js → model/FeatureSource.js} +0 -0
- /package/lib/{Perforator.js → model/Perforator.js} +0 -0
- /package/lib/{Quantity.d.ts → model/Quantity.d.ts} +0 -0
- /package/lib/{Quantity.js → model/Quantity.js} +0 -0
- /package/lib/{Symbol.js → model/Symbol.js} +0 -0
- /package/lib/{TrackCalibration.d.ts → model/TrackCalibration.d.ts} +0 -0
- /package/lib/{TrackCalibration.js → model/TrackCalibration.js} +0 -0
- /package/lib/{notes.d.ts → model/notes.d.ts} +0 -0
- /package/lib/{notes.js → model/notes.js} +0 -0
- /package/lib/{procedures.d.ts → model/procedures.d.ts} +0 -0
- /package/lib/{procedures.js → model/procedures.js} +0 -0
- /package/lib/{vocabulary.d.ts → model/vocabulary.d.ts} +0 -0
- /package/lib/{sorted.d.ts → shared/sorted.d.ts} +0 -0
- /package/lib/{sorted.js → shared/sorted.js} +0 -0
- /package/lib/{utils.d.ts → shared/utils.d.ts} +0 -0
- /package/lib/{utils.js → shared/utils.js} +0 -0
- /package/lib/{ReproducingSystem.js → systems/ReproducingSystem.js} +0 -0
package/lib/editionOps.js
DELETED
|
@@ -1,1081 +0,0 @@
|
|
|
1
|
-
import { current, isDraft } from "immer";
|
|
2
|
-
import { v4 } from "uuid";
|
|
3
|
-
import { getAt } from "./EditionView.js";
|
|
4
|
-
import { isCommand, placementRelations } from "./Symbol.js";
|
|
5
|
-
import { collationsOf, defaultCollationTolerance, isCollationsOwn, unchecked, uncheckedMotivation } from "./Collation.js";
|
|
6
|
-
import { collationToleranceOf, editsOf, insertedBy, principalDerivationOf } from "./Version.js";
|
|
7
|
-
import { asSymbols, barOf, featuresByAct, featuresOf, isPaperStretch, statesNothing } from "./RollCopy.js";
|
|
8
|
-
import { systemOf } from "./TrackerBar.js";
|
|
9
|
-
import { substitutionsBetween } from "./substitution.js";
|
|
10
|
-
import { trackerBarOf } from "./systems/index.js";
|
|
11
|
-
import { applyShift, applyScale, revertShift, revertScale, revertShortening, shortenChains } from "./alignment.js";
|
|
12
|
-
import { assignReference, idOf } from "./Assumption.js";
|
|
13
|
-
import { conditions as conditionsAllowed, featuresBorneBy, isPatch, withBorneFeatures } from "./Feature.js";
|
|
14
|
-
import { distance, mm, subtract } from "./Quantity.js";
|
|
15
|
-
const noChange = () => undefined;
|
|
16
|
-
const onCopy = (copyId, op) => draft => {
|
|
17
|
-
const copy = draft.copies.find(c => c.id === copyId);
|
|
18
|
-
if (copy)
|
|
19
|
-
op(copy, draft);
|
|
20
|
-
};
|
|
21
|
-
const onVersion = (versionId, op) => draft => {
|
|
22
|
-
const version = draft.versions.find(v => v.id === versionId);
|
|
23
|
-
if (version)
|
|
24
|
-
op(version, draft);
|
|
25
|
-
};
|
|
26
|
-
const onFeature = (copyId, featureId, op) => onCopy(copyId, copy => {
|
|
27
|
-
const feature = featuresOf(copy).find(f => f.id === featureId);
|
|
28
|
-
if (feature)
|
|
29
|
-
op(feature);
|
|
30
|
-
});
|
|
31
|
-
/**
|
|
32
|
-
* The state a draft stands at, as plain data. Reading a draft proxies
|
|
33
|
-
* everything it touches, so what is only read is read from this.
|
|
34
|
-
*/
|
|
35
|
-
const stateOf = (draft) => isDraft(draft) ? current(draft) : draft;
|
|
36
|
-
/** The items that do not match, or the very same array when none does, so that a draft stays untouched. */
|
|
37
|
-
const without = (items, matches) => items.some(matches) ? items.filter(item => !matches(item)) : items;
|
|
38
|
-
/** The items each changed, or the very same array when the change left every one as it was. */
|
|
39
|
-
const mapped = (items, change) => {
|
|
40
|
-
const changed = items.map(change);
|
|
41
|
-
return changed.every((item, i) => item === items[i]) ? items : changed;
|
|
42
|
-
};
|
|
43
|
-
/** The items each changed, less those the change emptied; the very same array where it changed none. */
|
|
44
|
-
const pruned = (items, change, emptied) => {
|
|
45
|
-
const changed = mapped(items, change);
|
|
46
|
-
return changed === items ? items : changed.filter((item, i) => item === items[i] || !emptied(item));
|
|
47
|
-
};
|
|
48
|
-
/** The record with the field replaced, or the very same record where that is what stood there. */
|
|
49
|
-
const replacing = (record, key, value) => record[key] === value ? record : { ...record, [key]: value };
|
|
50
|
-
/** The ids of the items the rewriting left out. */
|
|
51
|
-
const droppedIds = (before, after) => {
|
|
52
|
-
const kept = new Set(after.map(item => item.id));
|
|
53
|
-
return before.flatMap(item => kept.has(item.id) ? [] : [item.id]);
|
|
54
|
-
};
|
|
55
|
-
const insertion = (symbol) => ({ type: 'edit', id: v4(), insert: [symbol] });
|
|
56
|
-
const deletion = (symbolId) => ({ type: 'edit', id: v4(), delete: [symbolId] });
|
|
57
|
-
/** The edit as a collation leaves it: made by rule, and not yet read by anybody. */
|
|
58
|
-
const asUnchecked = (edit) => ({ ...edit, motivation: unchecked });
|
|
59
|
-
/**
|
|
60
|
-
* The motivations with the unchecked one declared, where any edit
|
|
61
|
-
* names it and the version does not state it yet. An edit naming a
|
|
62
|
-
* motivation the version leaves undeclared points at nothing.
|
|
63
|
-
*/
|
|
64
|
-
const declaring = (motivations, edits) => edits.some(edit => edit.motivation === unchecked)
|
|
65
|
-
&& !motivations.some(motivation => motivation.id === unchecked)
|
|
66
|
-
? [...motivations, uncheckedMotivation]
|
|
67
|
-
: motivations;
|
|
68
|
-
const isEmpty = (edit) => !edit.insert?.length && !edit.delete?.length;
|
|
69
|
-
const insertedIn = (versions) => versions.flatMap(insertedBy);
|
|
70
|
-
/** The edits with the change applied, less those it emptied; the very same array where it changed none. */
|
|
71
|
-
const edited = (edits, change) => pruned(edits, change, isEmpty);
|
|
72
|
-
/** The edit without the symbols among its insertions, or the very same edit where it inserts none of them. */
|
|
73
|
-
const droppingInsertions = (symbolIds) => (edit) => {
|
|
74
|
-
const insert = edit.insert && without(edit.insert, symbol => symbolIds.has(symbol.id));
|
|
75
|
-
return insert === edit.insert ? edit : { ...edit, insert };
|
|
76
|
-
};
|
|
77
|
-
/** Takes the symbols out of the version's own insertions, and the edits that had nothing else. */
|
|
78
|
-
const dropInsertions = (version, symbolIds) => {
|
|
79
|
-
const edits = stateOf(version).edits;
|
|
80
|
-
if (edits)
|
|
81
|
-
version.edits = edited(edits, droppingInsertions(symbolIds));
|
|
82
|
-
};
|
|
83
|
-
/**
|
|
84
|
-
* Puts the copy into the edition with a version of its own, which
|
|
85
|
-
* inserts every symbol the copy's own tracker bar reads on it. The
|
|
86
|
-
* version is a reading in that system's words, so it is coded for the
|
|
87
|
-
* system the copy was cut for.
|
|
88
|
-
*/
|
|
89
|
-
export const createVersion = (copy) => draft => {
|
|
90
|
-
const bar = barOf(copy);
|
|
91
|
-
draft.copies.push(copy);
|
|
92
|
-
draft.versions.push({
|
|
93
|
-
type: 'Version',
|
|
94
|
-
id: v4(),
|
|
95
|
-
system: systemOf(bar),
|
|
96
|
-
edits: asSymbols(featuresOf(copy), bar).map(insertion),
|
|
97
|
-
motivations: []
|
|
98
|
-
});
|
|
99
|
-
};
|
|
100
|
-
/**
|
|
101
|
-
* Puts the copy into the edition without a version of its own, as for a
|
|
102
|
-
* copy whose features are not read into symbols: one known only from a
|
|
103
|
-
* recording states instead which versions it carries.
|
|
104
|
-
*/
|
|
105
|
-
export const addCopy = (copy) => draft => {
|
|
106
|
-
draft.copies.push(copy);
|
|
107
|
-
};
|
|
108
|
-
/** States what the scale is put down to, in place of an earlier reading. */
|
|
109
|
-
const readScale = (copy, reading) => {
|
|
110
|
-
if (reading.cause === 'paper') {
|
|
111
|
-
copy.conditions = [...copy.conditions.filter(condition => !isPaperStretch(condition)), reading.condition];
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (!copy.production)
|
|
115
|
-
copy.production = {};
|
|
116
|
-
copy.production.speed = reading.speed;
|
|
117
|
-
};
|
|
118
|
-
/**
|
|
119
|
-
* Shifts and then scales the copy's features into line with another
|
|
120
|
-
* copy's, and puts the scale down to what the reading says: the paper,
|
|
121
|
-
* or the speed the copy was cut for.
|
|
122
|
-
*/
|
|
123
|
-
export const alignCopy = (copyId, shift, scale, reading) => onCopy(copyId, copy => {
|
|
124
|
-
applyShift(shift, copy);
|
|
125
|
-
applyScale(scale, copy);
|
|
126
|
-
if (reading)
|
|
127
|
-
readScale(copy, reading);
|
|
128
|
-
});
|
|
129
|
-
/**
|
|
130
|
-
* Puts the copy's features back where they were measured. A paper
|
|
131
|
-
* stretch read off the alignment goes with it; a speed stated stays,
|
|
132
|
-
* being a fact about the copy.
|
|
133
|
-
*/
|
|
134
|
-
export const unalignCopy = (copyId) => onCopy(copyId, copy => {
|
|
135
|
-
revertScale(copy);
|
|
136
|
-
revertShift(copy);
|
|
137
|
-
copy.conditions = without(copy.conditions, isPaperStretch);
|
|
138
|
-
});
|
|
139
|
-
/**
|
|
140
|
-
* Takes the extension a pneumatic reader adds off the ends of the
|
|
141
|
-
* copy's chains of holes, and records how much was taken, so that their lengths
|
|
142
|
-
* can be compared with a scanned copy's at all.
|
|
143
|
-
*/
|
|
144
|
-
export const shortenCopy = (copyId, extension, leaving) => onCopy(copyId, copy => shortenChains(extension, copy, leaving));
|
|
145
|
-
/** Puts the reader's extension back on the copy's chains of holes. */
|
|
146
|
-
export const unshortenCopy = (copyId) => onCopy(copyId, copy => revertShortening(copy));
|
|
147
|
-
/** States what the copy's features were read from, in place of any earlier statement. */
|
|
148
|
-
export const stateSource = (copyId, source) => onCopy(copyId, copy => {
|
|
149
|
-
copy.readFrom = source;
|
|
150
|
-
});
|
|
151
|
-
/** Takes back the statement, leaving the copy silent about its source again. */
|
|
152
|
-
export const clearSource = (copyId) => onCopy(copyId, copy => {
|
|
153
|
-
copy.readFrom = undefined;
|
|
154
|
-
});
|
|
155
|
-
/** Gives the copy the siglum it is referred to by, or takes it away where the siglum given is blank. */
|
|
156
|
-
export const nameCopy = (copyId, siglum) => onCopy(copyId, copy => {
|
|
157
|
-
const trimmed = siglum.trim();
|
|
158
|
-
if (trimmed)
|
|
159
|
-
copy.siglum = trimmed;
|
|
160
|
-
else
|
|
161
|
-
delete copy.siglum;
|
|
162
|
-
});
|
|
163
|
-
/** A reference under the belief given, where one is given. */
|
|
164
|
-
const referenceHeld = (id, belief) => ({ ...assignReference(id), ...(belief && { '@annotation': { id: v4(), belief } }) });
|
|
165
|
-
/** The references less those that match, or nothing where none is left. */
|
|
166
|
-
const withoutReferences = (references, matches) => {
|
|
167
|
-
const kept = references.filter(reference => !matches(reference));
|
|
168
|
-
return kept.length > 0 ? kept : undefined;
|
|
169
|
-
};
|
|
170
|
-
/** Takes out the copy's statements that match, and the list itself where none is left. */
|
|
171
|
-
const dropStatements = (copy, matches) => {
|
|
172
|
-
const statements = stateOf(copy).carries;
|
|
173
|
-
if (!statements?.some(matches))
|
|
174
|
-
return;
|
|
175
|
-
const kept = withoutReferences(statements, matches);
|
|
176
|
-
if (kept)
|
|
177
|
-
copy.carries = kept;
|
|
178
|
-
else
|
|
179
|
-
delete copy.carries;
|
|
180
|
-
};
|
|
181
|
-
/**
|
|
182
|
-
* States that the copy carries the version, under the belief given. It
|
|
183
|
-
* is meant for a copy whose features are not read into symbols, such as
|
|
184
|
-
* one known only from a recording, and `carriageProblems` reports it
|
|
185
|
-
* where features carry symbols already. A second statement about one
|
|
186
|
-
* version is none.
|
|
187
|
-
*/
|
|
188
|
-
export const stateCarriage = (copyId, versionId, belief) => onCopy(copyId, copy => {
|
|
189
|
-
const statements = stateOf(copy).carries ?? [];
|
|
190
|
-
if (statements.some(statement => idOf(statement) === versionId))
|
|
191
|
-
return;
|
|
192
|
-
copy.carries = [...statements, referenceHeld(versionId, belief)];
|
|
193
|
-
});
|
|
194
|
-
/** Takes back the copy's statement that it carries the version. */
|
|
195
|
-
export const clearCarriage = (copyId, versionId) => onCopy(copyId, copy => dropStatements(copy, statement => idOf(statement) === versionId));
|
|
196
|
-
/**
|
|
197
|
-
* Adds a general condition to the copy, beside whatever is stated of it
|
|
198
|
-
* already. The other condition a copy may be in, a paper stretch, is
|
|
199
|
-
* read off an alignment and stated by `alignCopy`.
|
|
200
|
-
*/
|
|
201
|
-
export const addGeneralCondition = (copyId, condition) => onCopy(copyId, copy => {
|
|
202
|
-
copy.conditions.push(condition);
|
|
203
|
-
});
|
|
204
|
-
/**
|
|
205
|
-
* Whether the feature's own kind allows a condition of this kind. The
|
|
206
|
-
* type narrowed to is the weaker statement, holding of a condition any
|
|
207
|
-
* kind of feature may be in.
|
|
208
|
-
*/
|
|
209
|
-
const allows = (feature, condition) => {
|
|
210
|
-
const allowed = conditionsAllowed[feature.type];
|
|
211
|
-
return allowed.includes(condition.conditionType);
|
|
212
|
-
};
|
|
213
|
-
/**
|
|
214
|
-
* States the condition of the feature, in place of any earlier
|
|
215
|
-
* statement. Throws where the kind of feature is in no such condition;
|
|
216
|
-
* `conditions` says which conditions each kind of feature may be in.
|
|
217
|
-
*/
|
|
218
|
-
export const stateFeatureCondition = (copyId, featureId, condition) => onFeature(copyId, featureId, feature => {
|
|
219
|
-
if (!allows(feature, condition)) {
|
|
220
|
-
throw new Error(`A ${feature.type} is in no '${condition.conditionType}' condition`);
|
|
221
|
-
}
|
|
222
|
-
feature.condition = condition;
|
|
223
|
-
});
|
|
224
|
-
/** The ids of every feature the copy bears, those a patch bears among them. */
|
|
225
|
-
const featureIdsOf = (copy) => new Set(featuresOf(copy).flatMap(withBorneFeatures).map(feature => feature.id));
|
|
226
|
-
/**
|
|
227
|
-
* A symbol every carrier of which lies among the features loses its
|
|
228
|
-
* evidence with them. A symbol without carriers, such as a label,
|
|
229
|
-
* stands on its own.
|
|
230
|
-
*/
|
|
231
|
-
const carriedOnlyOn = (features) => (symbol) => symbol.carriers.length > 0 && symbol.carriers.every(carrier => features.has(idOf(carrier)));
|
|
232
|
-
/** The symbols of the versions that no other copy carries. */
|
|
233
|
-
export const symbolsCarriedOnlyBy = (edition, copyId) => {
|
|
234
|
-
const copy = edition.copies.find(c => c.id === copyId);
|
|
235
|
-
return copy ? insertedIn(edition.versions).filter(carriedOnlyOn(featureIdsOf(copy))) : [];
|
|
236
|
-
};
|
|
237
|
-
const references = [...placementRelations, 'pairedWith'];
|
|
238
|
-
/** The command without its references to the dropped symbols, or the very same one where it makes none. */
|
|
239
|
-
const forgettingReferences = (command, dropped) => {
|
|
240
|
-
const stale = references.filter(relation => {
|
|
241
|
-
const reference = command[relation];
|
|
242
|
-
return reference !== undefined && dropped.has(idOf(reference));
|
|
243
|
-
});
|
|
244
|
-
if (stale.length === 0)
|
|
245
|
-
return command;
|
|
246
|
-
const kept = { ...command };
|
|
247
|
-
stale.forEach(relation => { delete kept[relation]; });
|
|
248
|
-
return kept;
|
|
249
|
-
};
|
|
250
|
-
/** The symbol without the features among its carriers, and without references to what went with them. */
|
|
251
|
-
const forgettingCarriers = (features, dropped) => (symbol) => {
|
|
252
|
-
const carriers = without(symbol.carriers, carrier => features.has(idOf(carrier)));
|
|
253
|
-
const relieved = carriers === symbol.carriers ? symbol : { ...symbol, carriers };
|
|
254
|
-
return isCommand(relieved) ? forgettingReferences(relieved, dropped) : relieved;
|
|
255
|
-
};
|
|
256
|
-
/** The edit without the dropped symbols and the features, or the very same edit where it had none of them. */
|
|
257
|
-
const forgettingFeatures = (features, dropped) => {
|
|
258
|
-
const forgetCarriers = forgettingCarriers(features, dropped);
|
|
259
|
-
return (edit) => {
|
|
260
|
-
const insert = edit.insert && mapped(without(edit.insert, symbol => dropped.has(symbol.id)), forgetCarriers);
|
|
261
|
-
const deleted = edit.delete && without(edit.delete, id => dropped.has(id));
|
|
262
|
-
if (insert === edit.insert && deleted === edit.delete)
|
|
263
|
-
return edit;
|
|
264
|
-
return { ...edit, ...(insert && { insert }), ...(deleted && { delete: deleted }) };
|
|
265
|
-
};
|
|
266
|
-
};
|
|
267
|
-
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
268
|
-
/** The record's values each changed, or the very same record where the change left every one as it was. */
|
|
269
|
-
const withValues = (record, change) => {
|
|
270
|
-
const entries = Object.entries(record);
|
|
271
|
-
const changed = entries.map(([key, value]) => [key, change(value)]);
|
|
272
|
-
return changed.every(([, value], i) => value === entries[i][1]) ? record : Object.fromEntries(changed);
|
|
273
|
-
};
|
|
274
|
-
/** The value with the change applied to every record within it and then to itself, innermost first. */
|
|
275
|
-
const deeply = (change) => {
|
|
276
|
-
const changed = (value) => Array.isArray(value) ? mapped(value, changed)
|
|
277
|
-
: isRecord(value) ? change(withValues(value, changed))
|
|
278
|
-
: value;
|
|
279
|
-
return changed;
|
|
280
|
-
};
|
|
281
|
-
/** Whether the two stand alike, so that what differs within them can be written where it lies. */
|
|
282
|
-
const alike = (before, after) => {
|
|
283
|
-
if (!isRecord(before) || !isRecord(after))
|
|
284
|
-
return false;
|
|
285
|
-
if (Array.isArray(before))
|
|
286
|
-
return Array.isArray(after) && before.length === after.length;
|
|
287
|
-
return !Array.isArray(after);
|
|
288
|
-
};
|
|
289
|
-
/** Writes onto the draft what the rewriting changed, as deep as the change reaches. */
|
|
290
|
-
const writeInto = (draft, before, after) => Object.entries(after).forEach(([key, value]) => {
|
|
291
|
-
if (value === before[key])
|
|
292
|
-
return;
|
|
293
|
-
if (alike(before[key], value))
|
|
294
|
-
writeInto(draft[key], before[key], value);
|
|
295
|
-
else
|
|
296
|
-
draft[key] = value;
|
|
297
|
-
});
|
|
298
|
-
const isCopy = (record) => 'type' in record && record.type === 'RollCopy';
|
|
299
|
-
const isBelief = (record) => 'type' in record && record.type === 'belief';
|
|
300
|
-
const isComprehension = (reason) => reason.type === 'meaningComprehension';
|
|
301
|
-
const comprehendsNothing = (reason) => isComprehension(reason) && reason.comprehends.length === 0;
|
|
302
|
-
/**
|
|
303
|
-
* The modification with what it names rewritten, or the very same one
|
|
304
|
-
* where that leaves it as it was. Only a removal names anything by id:
|
|
305
|
-
* what an act produced or glued on it states itself.
|
|
306
|
-
*/
|
|
307
|
-
const renamingMembers = (rename) => (modification) => modification.type === 'Removal'
|
|
308
|
-
? replacing(modification, 'removed', rename(modification.removed))
|
|
309
|
-
: modification;
|
|
310
|
-
/** The reason with what a comprehension comprehends rewritten; any other reason names nothing of the kind. */
|
|
311
|
-
const renamingComprehended = (rename) => (reason) => isComprehension(reason) ? replacing(reason, 'comprehends', rename(reason.comprehends)) : reason;
|
|
312
|
-
/**
|
|
313
|
-
* The record with the ids it names rewritten: what a copy's removals
|
|
314
|
-
* removed, and what the comprehensions among a belief's reasons
|
|
315
|
-
* comprehend. A removal or a comprehension the rewriting emptied goes
|
|
316
|
-
* with what it named; one that named nothing before stays, as an edit
|
|
317
|
-
* that was empty before it does.
|
|
318
|
-
*/
|
|
319
|
-
const renaming = (rename) => (record) => {
|
|
320
|
-
if (isCopy(record)) {
|
|
321
|
-
return replacing(record, 'modifications', pruned(record.modifications, renamingMembers(rename), statesNothing));
|
|
322
|
-
}
|
|
323
|
-
if (isBelief(record)) {
|
|
324
|
-
return replacing(record, 'reasons', pruned(record.reasons, renamingComprehended(rename), comprehendsNothing));
|
|
325
|
-
}
|
|
326
|
-
return record;
|
|
327
|
-
};
|
|
328
|
-
/**
|
|
329
|
-
* Rewrites the ids by which the edition names features and symbols
|
|
330
|
-
* outside the versions. A belief is annotatable anywhere, so the whole
|
|
331
|
-
* edition is read; what the rewriting leaves alone is left the very
|
|
332
|
-
* object it was.
|
|
333
|
-
*/
|
|
334
|
-
const renameReferences = (draft, rename) => {
|
|
335
|
-
const before = stateOf(draft);
|
|
336
|
-
const after = deeply(renaming(rename))(before);
|
|
337
|
-
if (after !== before)
|
|
338
|
-
writeInto(draft, before, after);
|
|
339
|
-
};
|
|
340
|
-
/**
|
|
341
|
-
* Strikes the features from the edition: their carriers go, a symbol
|
|
342
|
-
* that had no other carrier goes with them, an edit left exchanging
|
|
343
|
-
* nothing goes as well, and so does every reference the versions, the
|
|
344
|
-
* modifications of the copies and the comprehensions made to what went.
|
|
345
|
-
*/
|
|
346
|
-
const forgetFeatures = (draft, features) => {
|
|
347
|
-
const versions = stateOf(draft.versions);
|
|
348
|
-
const dropped = new Set(insertedIn(versions).filter(carriedOnlyOn(features)).map(symbol => symbol.id));
|
|
349
|
-
const forget = forgettingFeatures(features, dropped);
|
|
350
|
-
const edits = versions.map(version => version.edits && edited(version.edits, forget));
|
|
351
|
-
const gone = new Set([
|
|
352
|
-
...features,
|
|
353
|
-
...dropped,
|
|
354
|
-
...versions.flatMap((version, i) => droppedIds(editsOf(version), edits[i] ?? []))
|
|
355
|
-
]);
|
|
356
|
-
renameReferences(draft, ids => without(ids, id => gone.has(id)));
|
|
357
|
-
draft.versions.forEach((version, i) => {
|
|
358
|
-
const stated = edits[i];
|
|
359
|
-
if (stated)
|
|
360
|
-
version.edits = stated;
|
|
361
|
-
});
|
|
362
|
-
};
|
|
363
|
-
/**
|
|
364
|
-
* The ids of the features named and of everything they bear: a feature
|
|
365
|
-
* of a patch stands nowhere once the patch is gone.
|
|
366
|
-
*/
|
|
367
|
-
const goneWith = (features, named) => features.flatMap(feature => named.has(feature.id)
|
|
368
|
-
? withBorneFeatures(feature).map(borne => borne.id)
|
|
369
|
-
: goneWith(featuresBorneBy(feature), named));
|
|
370
|
-
/** The feature with the named ones gone from what it bears, or the very same one where it bears none of them. */
|
|
371
|
-
const withoutBorne = (feature, named) => {
|
|
372
|
-
if (!isPatch(feature) || !feature.features)
|
|
373
|
-
return feature;
|
|
374
|
-
const borne = withoutFeatures(feature.features, named);
|
|
375
|
-
return borne === feature.features ? feature : { ...feature, features: borne };
|
|
376
|
-
};
|
|
377
|
-
/** The features without those named, wherever they lie, and without whatever those bore. */
|
|
378
|
-
const withoutFeatures = (features, named) => mapped(without(features, feature => named.has(feature.id)), feature => withoutBorne(feature, named));
|
|
379
|
-
/** The act with its features rewritten, or the very same one where the rewriting left them. */
|
|
380
|
-
const rewritingAct = (rewrite) => (modification) => {
|
|
381
|
-
if (modification.type === 'Alteration')
|
|
382
|
-
return replacing(modification, 'produced', rewrite(modification.produced));
|
|
383
|
-
if (modification.type === 'Attachment')
|
|
384
|
-
return replacing(modification, 'added', rewrite(modification.added));
|
|
385
|
-
return modification;
|
|
386
|
-
};
|
|
387
|
-
/**
|
|
388
|
-
* Writes the rewriting onto the features of every act of the copy, and
|
|
389
|
-
* takes out an act it left with nothing to state.
|
|
390
|
-
*/
|
|
391
|
-
const rewriteFeatures = (copy, rewrite) => {
|
|
392
|
-
const state = stateOf(copy);
|
|
393
|
-
const produced = state.production?.produced;
|
|
394
|
-
if (produced && copy.production) {
|
|
395
|
-
const rewritten = rewrite(produced);
|
|
396
|
-
if (rewritten !== produced)
|
|
397
|
-
copy.production.produced = rewritten;
|
|
398
|
-
}
|
|
399
|
-
copy.modifications = pruned(state.modifications, rewritingAct(rewrite), statesNothing);
|
|
400
|
-
};
|
|
401
|
-
/** The features of the act that brought the named feature about, where that act produces features. */
|
|
402
|
-
const producedBeside = (copy, featureId) => {
|
|
403
|
-
const names = (features) => features.some(feature => feature.id === featureId);
|
|
404
|
-
const punched = copy.production?.produced;
|
|
405
|
-
if (punched && names(punched))
|
|
406
|
-
return punched;
|
|
407
|
-
const act = copy.modifications.find(act => act.type === 'Alteration' && names(act.produced));
|
|
408
|
-
return act?.type === 'Alteration' ? act.produced : undefined;
|
|
409
|
-
};
|
|
410
|
-
/** The patches of the attachment that glued the named one on. */
|
|
411
|
-
const gluedBeside = (copy, patchId) => {
|
|
412
|
-
const act = copy.modifications.find(act => act.type === 'Attachment' && act.added.some(patch => patch.id === patchId));
|
|
413
|
-
return act?.type === 'Attachment' ? act.added : undefined;
|
|
414
|
-
};
|
|
415
|
-
/** The list the copy states a new feature in, making the act it belongs to where there is none. */
|
|
416
|
-
const actFor = (copy, patch, act) => {
|
|
417
|
-
const beside = act.beside !== undefined
|
|
418
|
-
? (patch ? gluedBeside(copy, act.beside) : producedBeside(copy, act.beside))
|
|
419
|
-
: undefined;
|
|
420
|
-
if (beside)
|
|
421
|
-
return beside;
|
|
422
|
-
if (patch) {
|
|
423
|
-
const added = [];
|
|
424
|
-
copy.modifications.push({ type: 'Attachment', added, ...(act.purpose && { purpose: act.purpose }) });
|
|
425
|
-
return added;
|
|
426
|
-
}
|
|
427
|
-
if (act.punched) {
|
|
428
|
-
if (!copy.production)
|
|
429
|
-
copy.production = {};
|
|
430
|
-
if (!copy.production.produced)
|
|
431
|
-
copy.production.produced = [];
|
|
432
|
-
return copy.production.produced;
|
|
433
|
-
}
|
|
434
|
-
const produced = [];
|
|
435
|
-
copy.modifications.push({ type: 'Alteration', produced, ...(act.purpose && { purpose: act.purpose }) });
|
|
436
|
-
return produced;
|
|
437
|
-
};
|
|
438
|
-
/**
|
|
439
|
-
* States that the copy bears the feature, in the act that brought it
|
|
440
|
-
* about: a patch was glued on by an attachment, anything else was
|
|
441
|
-
* produced, by the punching or by a later act. Which act, `FeatureAct`
|
|
442
|
-
* says; a `beside` that names nothing the copy bears is passed over and
|
|
443
|
-
* the feature goes into an act of its own.
|
|
444
|
-
*/
|
|
445
|
-
export const addFeature = (copyId, feature, act = {}) => onCopy(copyId, copy => {
|
|
446
|
-
actFor(copy, isPatch(feature), act).push(feature);
|
|
447
|
-
});
|
|
448
|
-
/**
|
|
449
|
-
* States that the patch bears the feature. What a patch bears came onto
|
|
450
|
-
* the copy with the patch, so it belongs to no act of its own and
|
|
451
|
-
* states no place: it stands where the patch stands.
|
|
452
|
-
*/
|
|
453
|
-
export const addBorneFeature = (copyId, patchId, feature) => onCopy(copyId, copy => {
|
|
454
|
-
const patch = featuresOf(copy).flatMap(withBorneFeatures).find(borne => borne.id === patchId);
|
|
455
|
-
if (!patch || !isPatch(patch))
|
|
456
|
-
return;
|
|
457
|
-
if (!patch.features)
|
|
458
|
-
patch.features = [];
|
|
459
|
-
patch.features.push(feature);
|
|
460
|
-
});
|
|
461
|
-
/**
|
|
462
|
-
* Takes the features off the copy, and out of the versions with what
|
|
463
|
-
* only they carried. A feature may be named wherever the copy bears it:
|
|
464
|
-
* a patch goes with everything glued onto it, and a feature of a patch
|
|
465
|
-
* may be taken back on its own, the patch staying where it is. An act
|
|
466
|
-
* left having produced or added nothing goes as well.
|
|
467
|
-
*/
|
|
468
|
-
export const removeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
|
|
469
|
-
const named = new Set(featureIds);
|
|
470
|
-
const features = featuresOf(stateOf(copy));
|
|
471
|
-
rewriteFeatures(copy, (stated) => withoutFeatures(stated, named));
|
|
472
|
-
forgetFeatures(draft, new Set(goneWith(features, named)));
|
|
473
|
-
});
|
|
474
|
-
/**
|
|
475
|
-
* Takes the copy out of the edition together with the symbols only it
|
|
476
|
-
* carries, and with every reference the versions and the argumentations
|
|
477
|
-
* made to those symbols.
|
|
478
|
-
*/
|
|
479
|
-
export const removeCopy = (copyId) => onCopy(copyId, (copy, draft) => {
|
|
480
|
-
forgetFeatures(draft, featureIdsOf(copy));
|
|
481
|
-
draft.copies = draft.copies.filter(c => c.id !== copyId);
|
|
482
|
-
});
|
|
483
|
-
/**
|
|
484
|
-
* Whether two records of the edition state the same, the identity of a
|
|
485
|
-
* statement left out: two transcriptions reading the same word say the
|
|
486
|
-
* same, whichever ids they were given.
|
|
487
|
-
*/
|
|
488
|
-
const sayTheSame = (a, b) => {
|
|
489
|
-
if (a === b)
|
|
490
|
-
return true;
|
|
491
|
-
if (!isRecord(a) || !isRecord(b) || Array.isArray(a) !== Array.isArray(b))
|
|
492
|
-
return false;
|
|
493
|
-
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
494
|
-
keys.delete('id');
|
|
495
|
-
return [...keys].every(key => sayTheSame(a[key], b[key]));
|
|
496
|
-
};
|
|
497
|
-
/**
|
|
498
|
-
* What a feature states beyond its identity, its place along the roll,
|
|
499
|
-
* its depiction and its condition. What a patch bears counts by
|
|
500
|
-
* identity: two patches bear the same only where they bear the very
|
|
501
|
-
* same features, so that no merge takes a feature of a patch away.
|
|
502
|
-
*/
|
|
503
|
-
const nature = (feature) => {
|
|
504
|
-
const { id, horizontal, depiction, condition, ...rest } = feature;
|
|
505
|
-
return isPatch(feature) && feature.features
|
|
506
|
-
? { ...rest, features: feature.features.map(borne => borne.id) }
|
|
507
|
-
: rest;
|
|
508
|
-
};
|
|
509
|
-
const conditionsOf = (features) => features.flatMap(feature => feature.condition ? [feature.condition] : []);
|
|
510
|
-
/**
|
|
511
|
-
* What stands in the way of reading the features as one, or nothing
|
|
512
|
-
* where they may be merged. Features may be merged where they differ
|
|
513
|
-
* in nothing but their place along the roll, their depiction and the
|
|
514
|
-
* condition at most one of them states. How far apart they lie is not
|
|
515
|
-
* asked: whether a gap is a bridge of the perforator, a tear or two
|
|
516
|
-
* perforations of their own is the editor's reading.
|
|
517
|
-
*/
|
|
518
|
-
export const mergeObstacle = (features) => {
|
|
519
|
-
if (features.length < 2)
|
|
520
|
-
return 'fewer-than-two';
|
|
521
|
-
const [first, ...rest] = features;
|
|
522
|
-
if (rest.some(feature => feature.type !== first.type))
|
|
523
|
-
return 'different-types';
|
|
524
|
-
if (rest.some(feature => !sayTheSame(feature.vertical, first.vertical)))
|
|
525
|
-
return 'different-tracks';
|
|
526
|
-
if (rest.some(feature => !sayTheSame(nature(feature), nature(first))))
|
|
527
|
-
return 'unlike-features';
|
|
528
|
-
const conditions = conditionsOf(features);
|
|
529
|
-
if (conditions.some(condition => !sayTheSame(condition, conditions[0])))
|
|
530
|
-
return 'differing-conditions';
|
|
531
|
-
return undefined;
|
|
532
|
-
};
|
|
533
|
-
/** The acts of the copy that state any of the named features. */
|
|
534
|
-
const actsHolding = (copy, named) => featuresByAct(copy).filter(features => features.some(feature => named.has(feature.id)));
|
|
535
|
-
/**
|
|
536
|
-
* What stands in the way of reading the named features of the copy as
|
|
537
|
-
* one, which is the whole of what `mergeFeatures` asks: whether one act
|
|
538
|
-
* brought them all about, and what the features themselves say. An id
|
|
539
|
-
* the copy does not bear at a place of its own is passed over.
|
|
540
|
-
*/
|
|
541
|
-
const obstacleIn = (copy, featureIds) => {
|
|
542
|
-
const named = new Set(featureIds);
|
|
543
|
-
const acts = actsHolding(copy, named);
|
|
544
|
-
return acts.length > 1
|
|
545
|
-
? 'different-acts'
|
|
546
|
-
: mergeObstacle((acts[0] ?? []).filter(feature => named.has(feature.id)));
|
|
547
|
-
};
|
|
548
|
-
/**
|
|
549
|
-
* What stands in the way of reading the features the ids name as one,
|
|
550
|
-
* as `mergeFeatures` will find it. `mergeObstacle` asks only what the
|
|
551
|
-
* features themselves say, which cannot reach `different-acts`: one
|
|
552
|
-
* feature is the work of one act, and where the features stand is the
|
|
553
|
-
* edition's business rather than theirs.
|
|
554
|
-
*/
|
|
555
|
-
export const mergeObstacleIn = (view, featureIds) => {
|
|
556
|
-
const copy = featureIds.map(id => view.copyOf(id)).find(copy => copy !== undefined);
|
|
557
|
-
return copy ? obstacleIn(copy, featureIds) : 'fewer-than-two';
|
|
558
|
-
};
|
|
559
|
-
/** The items with the replacement in the place of the first it stands for, the others dropped. */
|
|
560
|
-
const standingFor = (items, stands, replacement) => {
|
|
561
|
-
const first = items.findIndex(stands);
|
|
562
|
-
if (first < 0)
|
|
563
|
-
return items;
|
|
564
|
-
return items.flatMap((item, i) => i === first ? [replacement] : stands(item) ? [] : [item]);
|
|
565
|
-
};
|
|
566
|
-
const spanning = (features) => ({
|
|
567
|
-
unit: 'mm',
|
|
568
|
-
from: mm(Math.min(...features.map(feature => feature.horizontal.from))),
|
|
569
|
-
to: mm(Math.max(...features.map(feature => feature.horizontal.to)))
|
|
570
|
-
});
|
|
571
|
-
/**
|
|
572
|
-
* The feature the merged ones give way to. It spans them all and is in
|
|
573
|
-
* every other respect the one of them that states a condition, the
|
|
574
|
-
* others being alike in all but their place. The depiction goes: a
|
|
575
|
-
* region of the scan showing one part depicts no more than that part.
|
|
576
|
-
*/
|
|
577
|
-
const mergedFrom = (features, id) => {
|
|
578
|
-
const stating = features.find(feature => feature.condition) ?? features[0];
|
|
579
|
-
const merged = { ...stating, id, horizontal: spanning(features) };
|
|
580
|
-
delete merged.depiction;
|
|
581
|
-
return merged;
|
|
582
|
-
};
|
|
583
|
-
/** The symbol carried by the merged feature where it named one of those it replaces, and naming it once. */
|
|
584
|
-
const carriedByMerged = (replaced, mergedId) => (symbol) => {
|
|
585
|
-
const names = (carrier) => replaced.has(idOf(carrier));
|
|
586
|
-
const first = symbol.carriers.find(names);
|
|
587
|
-
if (!first)
|
|
588
|
-
return symbol;
|
|
589
|
-
return { ...symbol, carriers: standingFor(symbol.carriers, names, { ...first, id: mergedId }) };
|
|
590
|
-
};
|
|
591
|
-
/** Points the versions at the merged feature wherever they name one it replaces. */
|
|
592
|
-
const carryOver = (draft, replaced, mergedId) => {
|
|
593
|
-
const recarry = carriedByMerged(replaced, mergedId);
|
|
594
|
-
const recarrying = (edit) => {
|
|
595
|
-
const insert = edit.insert && mapped(edit.insert, recarry);
|
|
596
|
-
return insert === edit.insert ? edit : { ...edit, insert };
|
|
597
|
-
};
|
|
598
|
-
const versions = stateOf(draft.versions);
|
|
599
|
-
draft.versions.forEach((version, i) => {
|
|
600
|
-
const edits = versions[i].edits;
|
|
601
|
-
if (edits)
|
|
602
|
-
version.edits = mapped(edits, recarrying);
|
|
603
|
-
});
|
|
604
|
-
};
|
|
605
|
-
/**
|
|
606
|
-
* Replaces the features of the copy with a single one covering them
|
|
607
|
-
* all, where a scan has split what the editor reads as one feature.
|
|
608
|
-
* The merged feature takes the place of the first it replaces and
|
|
609
|
-
* spans from the first to the last, any gap between them included.
|
|
610
|
-
* What the replaced features carried is carried by the merged one, and
|
|
611
|
-
* whatever else named them, a modification or a comprehension, names
|
|
612
|
-
* the merged one in their stead.
|
|
613
|
-
*
|
|
614
|
-
* Only the features the copy bears itself are merged. A feature of a
|
|
615
|
-
* patch states no place along the roll of its own, so there is nothing
|
|
616
|
-
* for a merge to span, and an id naming one is passed over as an id the
|
|
617
|
-
* copy does not bear is. Features of two different acts are not merged
|
|
618
|
-
* either: one feature is the work of one act.
|
|
619
|
-
*
|
|
620
|
-
* Throws where the features cannot stand for one; `mergeObstacle`
|
|
621
|
-
* says beforehand whether they can.
|
|
622
|
-
*/
|
|
623
|
-
export const mergeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
|
|
624
|
-
const state = stateOf(copy);
|
|
625
|
-
const obstacle = obstacleIn(state, featureIds);
|
|
626
|
-
if (obstacle) {
|
|
627
|
-
throw new Error(`The features of copy ${copyId} cannot be merged: ${obstacle}`);
|
|
628
|
-
}
|
|
629
|
-
const named = new Set(featureIds);
|
|
630
|
-
const toMerge = (actsHolding(state, named)[0] ?? []).filter(feature => named.has(feature.id));
|
|
631
|
-
const replaced = new Set(toMerge.map(feature => feature.id));
|
|
632
|
-
const mergedId = v4();
|
|
633
|
-
rewriteFeatures(copy, (stated) => {
|
|
634
|
-
const stands = (feature) => replaced.has(feature.id);
|
|
635
|
-
return stated.some(stands) ? standingFor(stated, stands, mergedFrom(stated.filter(stands), mergedId)) : stated;
|
|
636
|
-
});
|
|
637
|
-
carryOver(draft, replaced, mergedId);
|
|
638
|
-
renameReferences(draft, ids => standingFor(ids, id => replaced.has(id), mergedId));
|
|
639
|
-
});
|
|
640
|
-
/** The carriers of each collated symbol pass to its counterpart. */
|
|
641
|
-
const handOverCarriers = (view, draft, collations) => collations.forEach(({ symbol, counterpart }) => {
|
|
642
|
-
const path = view.getPath(counterpart.id);
|
|
643
|
-
const target = path && getAt(path, draft);
|
|
644
|
-
target?.carriers.push(...symbol.carriers);
|
|
645
|
-
});
|
|
646
|
-
/** Whether the two versions are coded for different reproducing systems. */
|
|
647
|
-
const differ = (child, parent) => {
|
|
648
|
-
const one = trackerBarOf(child?.system);
|
|
649
|
-
const other = trackerBarOf(parent?.system);
|
|
650
|
-
return one !== undefined && other !== undefined && one.id !== other.id;
|
|
651
|
-
};
|
|
652
|
-
/** Whether the version's text is read against the parent, its principal derivation naming it. */
|
|
653
|
-
const readsAgainst = (version, parentId) => {
|
|
654
|
-
const principal = principalDerivationOf(version);
|
|
655
|
-
return principal !== undefined && idOf(principal) === parentId;
|
|
656
|
-
};
|
|
657
|
-
/** The derivations the version states beside its principal one, less any naming the parent. */
|
|
658
|
-
const hypothesesBeside = (version, parentId) => {
|
|
659
|
-
const principal = principalDerivationOf(version);
|
|
660
|
-
return (version.basedOn ?? []).filter(derivation => derivation !== principal && idOf(derivation) !== parentId);
|
|
661
|
-
};
|
|
662
|
-
/** Takes out the derivations that match, and the list itself where none is left. */
|
|
663
|
-
const dropDerivations = (version, matches) => {
|
|
664
|
-
const derivations = stateOf(version).basedOn;
|
|
665
|
-
if (!derivations?.some(matches))
|
|
666
|
-
return;
|
|
667
|
-
const kept = withoutReferences(derivations, matches);
|
|
668
|
-
if (kept)
|
|
669
|
-
version.basedOn = kept;
|
|
670
|
-
else
|
|
671
|
-
delete version.basedOn;
|
|
672
|
-
};
|
|
673
|
-
/** The symbols a set of edits speaks for, whether by inserting or by deleting them. */
|
|
674
|
-
const spokenForBy = (edits) => new Set(edits.flatMap(edit => [
|
|
675
|
-
...(edit.insert ?? []).map(symbol => symbol.id),
|
|
676
|
-
...(edit.delete ?? [])
|
|
677
|
-
]));
|
|
678
|
-
/** An exchange of symbols, as a key: what it puts in against what it takes out. */
|
|
679
|
-
const exchangeKey = (inserted, deleted) => `${[...inserted].sort().join(',')}/${[...deleted].sort().join(',')}`;
|
|
680
|
-
/**
|
|
681
|
-
* The edits already stated, by the exchange each of them makes. An
|
|
682
|
-
* equivalence the collation draws a second time is the one already
|
|
683
|
-
* stated, and drawing it again is no reason to mint a new identifier or
|
|
684
|
-
* to drop the motivation somebody wrote on it.
|
|
685
|
-
*/
|
|
686
|
-
const byExchange = (edits) => new Map(edits.map(edit => [exchangeKey((edit.insert ?? []).map(symbol => symbol.id), edit.delete ?? []), edit]));
|
|
687
|
-
/**
|
|
688
|
-
* Bases the child on the parent. A symbol of the child that collates
|
|
689
|
-
* with one the parent hands down adds its carriers to that symbol; the
|
|
690
|
-
* rest become the child's insertions, and what the parent hands down
|
|
691
|
-
* and the child lacks becomes its deletions. The derivation states the
|
|
692
|
-
* tolerance it was collated at and becomes the principal one; the
|
|
693
|
-
* hypotheses the child stated beside its former one stay.
|
|
694
|
-
*
|
|
695
|
-
* The edits the child already stated are rewritten only as far as a
|
|
696
|
-
* collation wrote them (`isCollationsOwn`). An editor's reading of the
|
|
697
|
-
* difference between the two texts stays, and the symbols it speaks for
|
|
698
|
-
* are left out of the collation, so that connecting the two again
|
|
699
|
-
* neither doubles them nor silently drops what somebody established by
|
|
700
|
-
* hand. An equivalence is the collation's own, since it is drawn from
|
|
701
|
-
* the two systems' vocabularies rather than read off the paper, but one
|
|
702
|
-
* drawn again over the very same symbols is kept as it stands, with its
|
|
703
|
-
* identifier and whatever was written on it.
|
|
704
|
-
*
|
|
705
|
-
* The child's own text is what it shows less what the parent hands
|
|
706
|
-
* down, so that connecting two versions already connected collates the
|
|
707
|
-
* child's symbols and not the parent's with themselves.
|
|
708
|
-
*/
|
|
709
|
-
export const connectVersions = (view, childId, parentId, tolerance = defaultCollationTolerance) => {
|
|
710
|
-
const child = view.get(childId);
|
|
711
|
-
const stated = child ? editsOf(child) : [];
|
|
712
|
-
const established = stated.filter(edit => !isCollationsOwn(edit));
|
|
713
|
-
const spokenFor = spokenForBy(established);
|
|
714
|
-
const unspoken = (symbol) => !spokenFor.has(symbol.id);
|
|
715
|
-
// What the parent hands down and the child still shows passes
|
|
716
|
-
// through: it is neither the child's own symbol nor one it lacks.
|
|
717
|
-
// Without that, connecting a pair already connected would collate
|
|
718
|
-
// the inherited symbols with themselves and double their carriers.
|
|
719
|
-
const handedDown = view.snapshot(parentId).filter(unspoken);
|
|
720
|
-
const shown = new Set(view.snapshot(childId).map(symbol => symbol.id));
|
|
721
|
-
const inheritedIds = new Set(handedDown.map(symbol => symbol.id));
|
|
722
|
-
const inherited = handedDown.filter(symbol => !shown.has(symbol.id));
|
|
723
|
-
const own = view.snapshot(childId).filter(symbol => unspoken(symbol) && !inheritedIds.has(symbol.id));
|
|
724
|
-
const locate = (symbol) => view.placeOf(symbol);
|
|
725
|
-
const collations = collationsOf(own, inherited, locate, tolerance);
|
|
726
|
-
const collated = new Set(collations.map(({ symbol }) => symbol.id));
|
|
727
|
-
const matched = new Set(collations.map(({ counterpart }) => counterpart.id));
|
|
728
|
-
/**
|
|
729
|
-
* Where the child is coded for another system, a held command of
|
|
730
|
-
* its own often stands for a latched pair of the parent's. Saying so
|
|
731
|
-
* as one edit is the transfer being carried out, and leaving the two
|
|
732
|
-
* apart would make the apparatus a list of unexplained losses beside
|
|
733
|
-
* a list of unexplained gains.
|
|
734
|
-
*/
|
|
735
|
-
const substituted = differ(child, view.get(parentId))
|
|
736
|
-
? substitutionsBetween(own.filter(symbol => !collated.has(symbol.id)), inherited.filter(symbol => !matched.has(symbol.id)), locate, tolerance)
|
|
737
|
-
: [];
|
|
738
|
-
const paired = new Set(substituted.flatMap(({ replaced, by }) => [...by, ...replaced].map(symbol => symbol.id)));
|
|
739
|
-
const alreadyStated = byExchange(stated);
|
|
740
|
-
const equivalence = ({ replaced, by }) => {
|
|
741
|
-
const inserted = by.map(symbol => symbol.id);
|
|
742
|
-
const deleted = replaced.map(symbol => symbol.id);
|
|
743
|
-
return alreadyStated.get(exchangeKey(inserted, deleted)) ?? {
|
|
744
|
-
type: 'edit',
|
|
745
|
-
id: v4(),
|
|
746
|
-
editType: 'replace-with-equivalent',
|
|
747
|
-
motivation: unchecked,
|
|
748
|
-
insert: [...by],
|
|
749
|
-
delete: deleted
|
|
750
|
-
};
|
|
751
|
-
};
|
|
752
|
-
const edits = [
|
|
753
|
-
...established,
|
|
754
|
-
...substituted.map(equivalence),
|
|
755
|
-
...own.filter(symbol => !collated.has(symbol.id) && !paired.has(symbol.id)).map(insertion).map(asUnchecked),
|
|
756
|
-
...inherited
|
|
757
|
-
.filter(symbol => !matched.has(symbol.id) && !paired.has(symbol.id))
|
|
758
|
-
.map(symbol => asUnchecked(deletion(symbol.id)))
|
|
759
|
-
];
|
|
760
|
-
return onVersion(childId, (child, draft) => {
|
|
761
|
-
handOverCarriers(view, draft, collations);
|
|
762
|
-
child.edits = edits;
|
|
763
|
-
child.motivations = declaring(stateOf(child).motivations, edits);
|
|
764
|
-
child.basedOn = [
|
|
765
|
-
{ ...assignReference(parentId), collationTolerance: tolerance },
|
|
766
|
-
...hypothesesBeside(stateOf(child), parentId)
|
|
767
|
-
];
|
|
768
|
-
});
|
|
769
|
-
};
|
|
770
|
-
/**
|
|
771
|
-
* Folds the version's own symbols into those it inherits and collates
|
|
772
|
-
* with: the carriers pass over, and the insertions go. Collates at the
|
|
773
|
-
* tolerance of the derivation, where the caller names none.
|
|
774
|
-
*/
|
|
775
|
-
export const collateSymbols = (view, versionId, symbolIds, tolerance) => {
|
|
776
|
-
const version = view.get(versionId);
|
|
777
|
-
const principal = version && principalDerivationOf(version);
|
|
778
|
-
if (!version || !principal)
|
|
779
|
-
return noChange;
|
|
780
|
-
const chosen = new Set(symbolIds);
|
|
781
|
-
const own = insertedIn([version]).filter(symbol => chosen.has(symbol.id));
|
|
782
|
-
const collations = collationsOf(own, view.snapshot(idOf(principal)), symbol => view.placeOf(symbol), tolerance ?? collationToleranceOf(principal));
|
|
783
|
-
const collated = new Set(collations.map(({ symbol }) => symbol.id));
|
|
784
|
-
return onVersion(versionId, (version, draft) => {
|
|
785
|
-
handOverCarriers(view, draft, collations);
|
|
786
|
-
dropInsertions(version, collated);
|
|
787
|
-
});
|
|
788
|
-
};
|
|
789
|
-
/** The symbol as one copy reads it: what it says, on the given carriers, standing in no relation of its own. */
|
|
790
|
-
const readingOf = (symbol, carriers) => {
|
|
791
|
-
const identity = { id: v4(), carriers };
|
|
792
|
-
switch (symbol.type) {
|
|
793
|
-
case 'note':
|
|
794
|
-
return { type: 'note', pitch: symbol.pitch, ...identity };
|
|
795
|
-
case 'expression':
|
|
796
|
-
return { type: 'expression', expressionType: symbol.expressionType, scope: symbol.scope, ...identity };
|
|
797
|
-
case 'text':
|
|
798
|
-
return { type: 'text', text: symbol.text, ...identity };
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
/** The symbol where the copy and at least one other carry it, with the carriers of each side. */
|
|
802
|
-
/**
|
|
803
|
-
* The symbol where the named copies and at least one other carry it,
|
|
804
|
-
* with the carriers of each side.
|
|
805
|
-
*
|
|
806
|
-
* A symbol the named copies alone carry has no other side to be
|
|
807
|
-
* separated from and is passed over. That covers the insertions, and it
|
|
808
|
-
* covers a symbol an ancestor holds on one branch's witness alone,
|
|
809
|
-
* where the two versions do not disagree and there is nothing to take
|
|
810
|
-
* apart.
|
|
811
|
-
*/
|
|
812
|
-
const sharedWith = (view, symbol, copies) => {
|
|
813
|
-
const onNamedCopy = (carrier) => {
|
|
814
|
-
const copy = view.copyOf(idOf(carrier));
|
|
815
|
-
return copy !== undefined && copies.has(copy.id);
|
|
816
|
-
};
|
|
817
|
-
const mine = symbol.carriers.filter(onNamedCopy);
|
|
818
|
-
const theirs = symbol.carriers.filter(carrier => !onNamedCopy(carrier));
|
|
819
|
-
return mine.length > 0 && theirs.length > 0 ? [{ symbol, mine, theirs }] : [];
|
|
820
|
-
};
|
|
821
|
-
/**
|
|
822
|
-
* Takes the named copies' reading of a symbol back out of the symbol it
|
|
823
|
-
* was collated into: their carriers pass to a new symbol of the
|
|
824
|
-
* version's own, the remaining copies keep the symbol they had, and the
|
|
825
|
-
* version states the exchange.
|
|
826
|
-
*
|
|
827
|
-
* This is the inverse of the hand-over a collation makes, and it is
|
|
828
|
-
* what lets a derivation be collated a second time. A collated symbol
|
|
829
|
-
* is one symbol carrying every copy that reads it, so nothing else
|
|
830
|
-
* takes the two readings apart again, and without that a tolerance
|
|
831
|
-
* arrived at after the fact could never be applied.
|
|
832
|
-
*
|
|
833
|
-
* The copies named are one **side** of the derivation, the same side a
|
|
834
|
-
* window is measured over, and `sidesOf` is what says which they are.
|
|
835
|
-
* Naming one copy of a side that has several does not separate that
|
|
836
|
-
* side: what stays behind is the rest of it, mixed with the other
|
|
837
|
-
* side's copies, and a collation then compares one copy against that
|
|
838
|
-
* mixture rather than the two texts against each other.
|
|
839
|
-
*
|
|
840
|
-
* Symbols the named copies alone carry are already the version's own
|
|
841
|
-
* and are passed over, so a reading somebody has separated by hand
|
|
842
|
-
* keeps its identifier and the edit that speaks for it. Named symbols
|
|
843
|
-
* narrow the act to those; naming none separates the whole reading.
|
|
844
|
-
*/
|
|
845
|
-
export const separateReadings = (view, versionId, copies, symbolIds) => {
|
|
846
|
-
const version = view.get(versionId);
|
|
847
|
-
if (!version)
|
|
848
|
-
return noChange;
|
|
849
|
-
const chosen = symbolIds && new Set(symbolIds);
|
|
850
|
-
const shared = view.snapshot(versionId)
|
|
851
|
-
.filter(symbol => chosen === undefined || chosen.has(symbol.id))
|
|
852
|
-
.flatMap(symbol => sharedWith(view, symbol, copies));
|
|
853
|
-
if (shared.length === 0)
|
|
854
|
-
return noChange;
|
|
855
|
-
const inserted = new Set(insertedBy(version).map(symbol => symbol.id));
|
|
856
|
-
const stated = editsOf(version);
|
|
857
|
-
const separations = shared.map(({ symbol, mine }) => ({
|
|
858
|
-
type: 'edit',
|
|
859
|
-
id: v4(),
|
|
860
|
-
insert: [readingOf(symbol, [...mine])],
|
|
861
|
-
// A symbol the version inserts itself stays where it is, minus
|
|
862
|
-
// the carriers that leave it. Only one it inherits is exchanged.
|
|
863
|
-
...(inserted.has(symbol.id) ? {} : { delete: [symbol.id] })
|
|
864
|
-
}));
|
|
865
|
-
return onVersion(versionId, (version, draft) => {
|
|
866
|
-
shared.forEach(({ symbol, theirs }) => {
|
|
867
|
-
const path = view.getPath(symbol.id);
|
|
868
|
-
const target = path && getAt(path, draft);
|
|
869
|
-
if (target)
|
|
870
|
-
target.carriers = theirs;
|
|
871
|
-
});
|
|
872
|
-
version.edits = [...stated, ...separations];
|
|
873
|
-
});
|
|
874
|
-
};
|
|
875
|
-
/**
|
|
876
|
-
* Makes the version stand on its own: what it inherited becomes its
|
|
877
|
-
* own insertions, and its derivations go, the hypotheses among them,
|
|
878
|
-
* with the motivations that belonged to them.
|
|
879
|
-
*/
|
|
880
|
-
export const detachVersion = (view, versionId) => {
|
|
881
|
-
const edits = view.snapshot(versionId).map(insertion);
|
|
882
|
-
return onVersion(versionId, version => {
|
|
883
|
-
version.edits = edits;
|
|
884
|
-
delete version.basedOn;
|
|
885
|
-
version.motivations = [];
|
|
886
|
-
});
|
|
887
|
-
};
|
|
888
|
-
/**
|
|
889
|
-
* Takes the version out. Whatever read its text against it comes to
|
|
890
|
-
* stand on its own, a hypothesis that something derives from it goes,
|
|
891
|
-
* and so does a copy's statement that it carries the version.
|
|
892
|
-
*/
|
|
893
|
-
export const removeVersion = (view, versionId) => {
|
|
894
|
-
const detachments = view.edition.versions
|
|
895
|
-
.filter(version => readsAgainst(version, versionId))
|
|
896
|
-
.map(version => detachVersion(view, version.id));
|
|
897
|
-
const namesIt = (reference) => idOf(reference) === versionId;
|
|
898
|
-
return draft => {
|
|
899
|
-
detachments.forEach(detach => detach(draft));
|
|
900
|
-
draft.versions.forEach(version => dropDerivations(version, namesIt));
|
|
901
|
-
draft.copies.forEach(copy => dropStatements(copy, namesIt));
|
|
902
|
-
draft.versions = without(draft.versions, version => version.id === versionId);
|
|
903
|
-
};
|
|
904
|
-
};
|
|
905
|
-
/** Takes the symbols out of the version's own insertions, and the edits that had nothing else. */
|
|
906
|
-
export const removeSymbols = (versionId, symbolIds) => onVersion(versionId, version => dropInsertions(version, new Set(symbolIds)));
|
|
907
|
-
/** Moves the edits into a new version based on this one. */
|
|
908
|
-
export const deriveVersion = (versionId, editIds) => onVersion(versionId, (version, draft) => {
|
|
909
|
-
const chosen = new Set(editIds);
|
|
910
|
-
const moved = editsOf(version).filter(edit => chosen.has(edit.id));
|
|
911
|
-
if (version.edits)
|
|
912
|
-
version.edits = without(version.edits, edit => chosen.has(edit.id));
|
|
913
|
-
draft.versions.push({
|
|
914
|
-
type: 'Version',
|
|
915
|
-
id: v4(),
|
|
916
|
-
system: stateOf(version).system,
|
|
917
|
-
basedOn: [assignReference(versionId)],
|
|
918
|
-
edits: moved,
|
|
919
|
-
motivations: []
|
|
920
|
-
});
|
|
921
|
-
});
|
|
922
|
-
/**
|
|
923
|
-
* States that the version may also derive from the parent, beside what
|
|
924
|
-
* it derives from already: a hypothesis, such as a contamination, under
|
|
925
|
-
* the belief given. The text stays read against the principal derivation
|
|
926
|
-
* unless the belief holds this one more certain. A version derives from
|
|
927
|
-
* itself, or twice from one parent, in no statement.
|
|
928
|
-
*/
|
|
929
|
-
export const stateDerivation = (versionId, parentId, belief) => onVersion(versionId, version => {
|
|
930
|
-
const derivations = stateOf(version).basedOn ?? [];
|
|
931
|
-
if (parentId === versionId || derivations.some(derivation => idOf(derivation) === parentId))
|
|
932
|
-
return;
|
|
933
|
-
version.basedOn = [...derivations, referenceHeld(parentId, belief)];
|
|
934
|
-
});
|
|
935
|
-
/** Takes back the hypothesis that the version derives from the parent; the principal derivation goes with `detachVersion`. */
|
|
936
|
-
export const clearDerivation = (versionId, parentId) => onVersion(versionId, version => {
|
|
937
|
-
if (readsAgainst(stateOf(version), parentId))
|
|
938
|
-
return;
|
|
939
|
-
dropDerivations(version, derivation => idOf(derivation) === parentId);
|
|
940
|
-
});
|
|
941
|
-
const sameSequence = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
|
|
942
|
-
const expressionTypesOf = (symbols) => symbols.filter((symbol) => symbol.type === 'expression').map(symbol => symbol.expressionType);
|
|
943
|
-
/**
|
|
944
|
-
* How a single added accent is spelled. The red Welte latches a valve
|
|
945
|
-
* on and off again, the green holds one perforation for as long as the
|
|
946
|
-
* accent lasts, so the spelling is the system's and not the music's.
|
|
947
|
-
* Which of them a bar can spell decides which ones it is offered.
|
|
948
|
-
*/
|
|
949
|
-
const ACCENTS = [
|
|
950
|
-
['SlowCrescendoOn', 'SlowCrescendoOff'],
|
|
951
|
-
['ForzandoOn', 'ForzandoOff'],
|
|
952
|
-
['Crescendo'],
|
|
953
|
-
['SforzandoForte']
|
|
954
|
-
];
|
|
955
|
-
const accentsOn = (bar) => bar ? ACCENTS.filter(accent => accent.every(type => bar.expressionTypes.includes(type))) : [];
|
|
956
|
-
const lengthOf = (span) => subtract(span.to, span.from);
|
|
957
|
-
/** How far apart two onsets may lie for the one symbol to count as a replacement of the other. */
|
|
958
|
-
const REPLACEMENT_TOLERANCE = mm(5);
|
|
959
|
-
/** Shorten or prolong, where the inserted symbol starts about where the deleted one did. */
|
|
960
|
-
const replacementType = (view, inserted, deleted) => {
|
|
961
|
-
const after = view.placeOf(inserted);
|
|
962
|
-
const before = view.placeOf(deleted);
|
|
963
|
-
if (!after || !before || distance(after.from, before.from) >= REPLACEMENT_TOLERANCE)
|
|
964
|
-
return undefined;
|
|
965
|
-
return lengthOf(after) < lengthOf(before) ? 'shorten' : 'prolong';
|
|
966
|
-
};
|
|
967
|
-
/**
|
|
968
|
-
* A guess at what an edit does, from the symbols it exchanges and from
|
|
969
|
-
* the systems the version and the one it is based on are coded for.
|
|
970
|
-
*/
|
|
971
|
-
const guessEditType = (view, versionId, edit) => {
|
|
972
|
-
const inserts = edit.insert ?? [];
|
|
973
|
-
const deletes = view.getAll(edit.delete ?? []);
|
|
974
|
-
const inserted = expressionTypesOf(inserts);
|
|
975
|
-
const deleted = expressionTypesOf(deletes);
|
|
976
|
-
const bar = trackerBarOf(view.get(versionId)?.system);
|
|
977
|
-
const parentBar = trackerBarOf(view.predecessorOf(versionId)?.system);
|
|
978
|
-
/**
|
|
979
|
-
* Where the version is coded for another system than its parent, an
|
|
980
|
-
* exchange of expression matter is the transfer being carried out:
|
|
981
|
-
* a red ForzandoOn and ForzandoOff pair giving way to one held green
|
|
982
|
-
* SforzandoForte says the same thing in the other system's words,
|
|
983
|
-
* which is what 'replace-with-equivalent' is for. Calling it a
|
|
984
|
-
* corrected error would say the editor made a mistake.
|
|
985
|
-
*/
|
|
986
|
-
if (bar && parentBar && bar.id !== parentBar.id && inserted.length > 0 && deleted.length > 0) {
|
|
987
|
-
return 'replace-with-equivalent';
|
|
988
|
-
}
|
|
989
|
-
if (deleted.length === 0 && accentsOn(bar).some(accent => sameSequence(inserted, accent)))
|
|
990
|
-
return 'additional-accent';
|
|
991
|
-
if (inserted.length > 1 && sameSequence(inserted, deleted))
|
|
992
|
-
return 'shift';
|
|
993
|
-
if (inserted.length === 0 && deleted.length === 1)
|
|
994
|
-
return 'remove-redundancy';
|
|
995
|
-
if (inserts.length === 1 && deletes.length === 1)
|
|
996
|
-
return replacementType(view, inserts[0], deletes[0]) ?? 'correct-error';
|
|
997
|
-
return 'correct-error';
|
|
998
|
-
};
|
|
999
|
-
/**
|
|
1000
|
-
* Replaces the edits with a single one carrying all their insertions
|
|
1001
|
-
* and deletions, classified by a guess at what the exchange does.
|
|
1002
|
-
*/
|
|
1003
|
-
export const mergeEdits = (view, versionId, toMerge) => {
|
|
1004
|
-
if (toMerge.length === 0)
|
|
1005
|
-
return noChange;
|
|
1006
|
-
const merged = {
|
|
1007
|
-
...toMerge[0],
|
|
1008
|
-
id: v4(),
|
|
1009
|
-
insert: toMerge.flatMap(edit => edit.insert ?? []),
|
|
1010
|
-
delete: toMerge.flatMap(edit => edit.delete ?? [])
|
|
1011
|
-
};
|
|
1012
|
-
merged.editType = guessEditType(view, versionId, merged);
|
|
1013
|
-
const mergedIds = new Set(toMerge.map(edit => edit.id));
|
|
1014
|
-
return onVersion(versionId, version => {
|
|
1015
|
-
version.edits = [...editsOf(version).filter(edit => !mergedIds.has(edit.id)), merged];
|
|
1016
|
-
});
|
|
1017
|
-
};
|
|
1018
|
-
/** Replaces the edit with one edit per inserted and one per deleted symbol. */
|
|
1019
|
-
export const splitEdit = (versionId, toSplit) => {
|
|
1020
|
-
const parts = [
|
|
1021
|
-
...(toSplit.insert ?? []).map(insertion),
|
|
1022
|
-
...(toSplit.delete ?? []).map(deletion)
|
|
1023
|
-
];
|
|
1024
|
-
return onVersion(versionId, version => {
|
|
1025
|
-
version.edits = [...editsOf(version).filter(edit => edit.id !== toSplit.id), ...parts];
|
|
1026
|
-
});
|
|
1027
|
-
};
|
|
1028
|
-
/**
|
|
1029
|
-
* Runs the change on the command the view locates by id, in whichever
|
|
1030
|
-
* version inserted it. A statement made there holds in every version
|
|
1031
|
-
* that carries the command.
|
|
1032
|
-
*/
|
|
1033
|
-
const onCommand = (view, id, op) => draft => {
|
|
1034
|
-
const path = view.getPath(id);
|
|
1035
|
-
const symbol = path && getAt(path, draft);
|
|
1036
|
-
if (isCommand(symbol))
|
|
1037
|
-
op(symbol);
|
|
1038
|
-
};
|
|
1039
|
-
const clearPlacement = (command) => placementRelations.forEach(relation => { delete command[relation]; });
|
|
1040
|
-
/** States how the follower is placed relative to the reference, in place of any earlier statement. */
|
|
1041
|
-
export const placeCommand = (view, followerId, referenceId, relation) => onCommand(view, followerId, command => {
|
|
1042
|
-
clearPlacement(command);
|
|
1043
|
-
command[relation] = assignReference(referenceId);
|
|
1044
|
-
});
|
|
1045
|
-
export const unplaceCommand = (view, followerId) => onCommand(view, followerId, clearPlacement);
|
|
1046
|
-
/** The pair is stated on `statingId` only, as the format asks. */
|
|
1047
|
-
export const pairCommands = (view, statingId, partnerId) => onCommand(view, statingId, command => {
|
|
1048
|
-
command.pairedWith = assignReference(partnerId);
|
|
1049
|
-
});
|
|
1050
|
-
export const unpairCommand = (view, statingId) => onCommand(view, statingId, command => {
|
|
1051
|
-
delete command.pairedWith;
|
|
1052
|
-
});
|
|
1053
|
-
const onAssumptionAt = (path, op) => draft => {
|
|
1054
|
-
const assumption = getAt(path, draft);
|
|
1055
|
-
if (assumption)
|
|
1056
|
-
op(assumption);
|
|
1057
|
-
};
|
|
1058
|
-
const onBeliefAt = (path, op) => onAssumptionAt(path, assumption => {
|
|
1059
|
-
const belief = assumption['@annotation']?.belief;
|
|
1060
|
-
if (belief)
|
|
1061
|
-
op(belief);
|
|
1062
|
-
});
|
|
1063
|
-
/** Annotates the assumption at the path with a belief held true, for reasons to be added. */
|
|
1064
|
-
export const createBelief = (path) => onAssumptionAt(path, assumption => {
|
|
1065
|
-
assumption['@annotation'] = {
|
|
1066
|
-
id: v4(),
|
|
1067
|
-
belief: { type: 'belief', id: v4(), certainty: 'true', reasons: [] }
|
|
1068
|
-
};
|
|
1069
|
-
});
|
|
1070
|
-
export const clearBelief = (path) => onAssumptionAt(path, assumption => {
|
|
1071
|
-
delete assumption['@annotation'];
|
|
1072
|
-
});
|
|
1073
|
-
export const setCertainty = (path, certainty) => onBeliefAt(path, belief => {
|
|
1074
|
-
belief.certainty = certainty;
|
|
1075
|
-
});
|
|
1076
|
-
export const addReason = (path, reason) => onBeliefAt(path, belief => {
|
|
1077
|
-
belief.reasons.push(reason);
|
|
1078
|
-
});
|
|
1079
|
-
export const removeReason = (path, index) => onBeliefAt(path, belief => {
|
|
1080
|
-
belief.reasons.splice(index, 1);
|
|
1081
|
-
});
|