linked-rolls 0.19.1 → 0.20.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/Collation.d.ts +1 -3
- package/lib/Collation.js +9 -11
- package/lib/Edition.d.ts +1 -9
- package/lib/EditionView.d.ts +54 -6
- package/lib/EditionView.js +93 -20
- package/lib/Emulation.js +1 -1
- package/lib/RollCopy.d.ts +17 -2
- package/lib/RollCopy.js +18 -2
- package/lib/TrackerBar.d.ts +17 -2
- package/lib/TrackerBar.js +22 -16
- package/lib/Version.d.ts +49 -1
- package/lib/asJsonLd.js +23 -8
- package/lib/constraints.d.ts +8 -19
- package/lib/constraints.js +71 -15
- package/lib/context.d.ts +8 -1
- package/lib/context.js +8 -1
- package/lib/editionOps.d.ts +3 -1
- package/lib/editionOps.js +77 -15
- package/lib/importJsonLd.js +4 -2
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/migrate.js +85 -11
- package/lib/readers/phillipsEroll.d.ts +3 -5
- package/lib/readers/phillipsEroll.js +6 -7
- package/lib/readers/spencerBar.d.ts +4 -5
- package/lib/readers/spencerBar.js +7 -8
- package/lib/readers/stanfordAton.d.ts +4 -7
- package/lib/readers/stanfordAton.js +4 -5
- package/lib/reservations.d.ts +1 -1
- package/lib/reservations.js +24 -2
- package/lib/schema.json +27 -5
- package/lib/spec/context.json +1 -0
- package/lib/spec/welte-licensee.context.json +10 -0
- package/lib/substitution.d.ts +73 -0
- package/lib/substitution.js +175 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { v4 } from "uuid";
|
|
2
2
|
import { AtonParser } from "./AtonParser";
|
|
3
|
-
import { systemOf
|
|
3
|
+
import { systemOf } from "../TrackerBar";
|
|
4
4
|
import { welteT100 } from "../systems/welteT100/bar";
|
|
5
5
|
import { inMillimeters, mean, mm, pixelsPerInch, px, subtract, track } from "../Quantity";
|
|
6
6
|
/** Values in these files carry their unit as a suffix, e.g. "37.7646px". */
|
|
@@ -110,7 +110,7 @@ const measuredByOf = (rollinfo) => {
|
|
|
110
110
|
date
|
|
111
111
|
};
|
|
112
112
|
};
|
|
113
|
-
export function readFromStanfordAton(atonString, { trackShift,
|
|
113
|
+
export function readFromStanfordAton(atonString, { trackShift, system = welteT100, scan } = {}) {
|
|
114
114
|
const parser = new AtonParser();
|
|
115
115
|
const json = parser.parse(atonString);
|
|
116
116
|
const holes = json.ROLLINFO.HOLES.HOLE;
|
|
@@ -128,11 +128,10 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
|
|
|
128
128
|
};
|
|
129
129
|
const punchDiameter = punchDiameterOf(holes, dpi);
|
|
130
130
|
const chains = chainsAmong([...holes, ...chainedBadHoles(listOf(json.ROLLINFO.BADHOLES?.HOLE), calibration)]);
|
|
131
|
-
const onBar = translationBetween(system, bar);
|
|
132
131
|
const features = chains
|
|
133
132
|
.flatMap(({ hole, attack, release }) => {
|
|
134
|
-
const position =
|
|
135
|
-
if (position
|
|
133
|
+
const position = track(+hole.TRACKER_HOLE + shift);
|
|
134
|
+
if (!system.meaningOf(position))
|
|
136
135
|
return [];
|
|
137
136
|
const column = readPx(hole.ORIGIN_COL);
|
|
138
137
|
const columnWidth = readPx(hole.WIDTH_COL);
|
package/lib/reservations.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RollCopy } from "./RollCopy";
|
|
2
|
-
export declare const reservationTypes: readonly ['source-not-stated', 'source-undocumented', 'features-interpreted', 'no-physical-evidence', 'measurement-undocumented', 'not-calibrated'];
|
|
2
|
+
export declare const reservationTypes: readonly ['source-not-stated', 'source-undocumented', 'features-interpreted', 'no-physical-evidence', 'measurement-undocumented', 'not-calibrated', 'system-unknown'];
|
|
3
3
|
export type ReservationType = typeof reservationTypes[number];
|
|
4
4
|
/**
|
|
5
5
|
* Something an edition cannot vouch for in one of its copies.
|
package/lib/reservations.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { bearsPhysicalEvidence, isMeasured, sourceLabels } from "./FeatureSource";
|
|
2
2
|
import { calibrationOf } from "./RollCopy";
|
|
3
|
+
import { trackerBarOf } from "./systems";
|
|
3
4
|
export const reservationTypes = [
|
|
4
5
|
'source-not-stated',
|
|
5
6
|
'source-undocumented',
|
|
6
7
|
'features-interpreted',
|
|
7
8
|
'no-physical-evidence',
|
|
8
9
|
'measurement-undocumented',
|
|
9
|
-
'not-calibrated'
|
|
10
|
+
'not-calibrated',
|
|
11
|
+
'system-unknown'
|
|
10
12
|
];
|
|
11
13
|
const sourceStated = copy => copy.readFrom ? undefined : {
|
|
12
14
|
type: 'source-not-stated',
|
|
@@ -37,13 +39,33 @@ const calibrated = copy => calibrationOf(copy) ? undefined : {
|
|
|
37
39
|
type: 'not-calibrated',
|
|
38
40
|
note: 'The copy is not calibrated against the tracker bar, so its track positions rest on the numbering its source used.'
|
|
39
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* The bar decides what every track on the copy means, so a copy the
|
|
44
|
+
* edition cannot place in a system is read by the T-100 for want of
|
|
45
|
+
* anything better. Between two Welte scales that is a semitone rather
|
|
46
|
+
* than a visible error, which is why it is said out loud.
|
|
47
|
+
*/
|
|
48
|
+
const systemKnown = copy => {
|
|
49
|
+
const system = copy.production?.system;
|
|
50
|
+
if (system === undefined) {
|
|
51
|
+
return {
|
|
52
|
+
type: 'system-unknown',
|
|
53
|
+
note: 'The copy names no reproducing system, so it is read by the T-100 tracker bar.'
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return trackerBarOf(system) ? undefined : {
|
|
57
|
+
type: 'system-unknown',
|
|
58
|
+
note: `The copy names ${system.name || 'a reproducing system'}, which the edition has no tracker bar for, so it is read by the T-100's.`
|
|
59
|
+
};
|
|
60
|
+
};
|
|
40
61
|
const checks = [
|
|
41
62
|
sourceStated,
|
|
42
63
|
sourceDocumented,
|
|
43
64
|
featuresMeasured,
|
|
44
65
|
physicalEvidence,
|
|
45
66
|
measurementDocumented,
|
|
46
|
-
calibrated
|
|
67
|
+
calibrated,
|
|
68
|
+
systemKnown
|
|
47
69
|
];
|
|
48
70
|
/**
|
|
49
71
|
* What the edition cannot vouch for in a copy, in the order the
|
package/lib/schema.json
CHANGED
|
@@ -1802,15 +1802,10 @@
|
|
|
1802
1802
|
"recordingEvent": {
|
|
1803
1803
|
"$ref": "#/definitions/RecordingEvent",
|
|
1804
1804
|
"description": "[ontology: lrmoo:R19i was realised through]"
|
|
1805
|
-
},
|
|
1806
|
-
"system": {
|
|
1807
|
-
"$ref": "#/definitions/Concept",
|
|
1808
|
-
"description": "The reproducing system the roll was cut for. A system the type vocabulary knows carries the IRI of its concept as `id`, from which the export takes the system's own context, so that the expression types are read as that system's. [ontology: crm:P2 has type]"
|
|
1809
1805
|
}
|
|
1810
1806
|
},
|
|
1811
1807
|
"required": [
|
|
1812
1808
|
"catalogueNumber",
|
|
1813
|
-
"system",
|
|
1814
1809
|
"recordingEvent"
|
|
1815
1810
|
],
|
|
1816
1811
|
"type": "object"
|
|
@@ -2089,6 +2084,10 @@
|
|
|
2089
2084
|
"$ref": "#/definitions/Derivation",
|
|
2090
2085
|
"description": "If no derivation is defined, it is assumed that this version represents the mother roll. [ontology: lrmoo:R76 is derivative of]"
|
|
2091
2086
|
},
|
|
2087
|
+
"creation": {
|
|
2088
|
+
"$ref": "#/definitions/VersionCreation",
|
|
2089
|
+
"description": "The act that made this version, where it is known: who carried it out, when, and by what rule. [ontology: lrmoo:R17i was created by]"
|
|
2090
|
+
},
|
|
2092
2091
|
"edits": {
|
|
2093
2092
|
"description": "The list of edits that, applied to the base version, produce this version. [ontology: reo:involvedEdit]",
|
|
2094
2093
|
"items": {
|
|
@@ -2107,6 +2106,10 @@
|
|
|
2107
2106
|
"description": "A short siglum to identify the version, e.g. \"A\", \"B1\", \"B2_rev\", etc. [ontology: reo:siglum]",
|
|
2108
2107
|
"type": "string"
|
|
2109
2108
|
},
|
|
2109
|
+
"system": {
|
|
2110
|
+
"$ref": "#/definitions/Concept",
|
|
2111
|
+
"description": "The reproducing system this version is coded for. One roll was often issued for several of them, and a version is a reading in one system's words: its expression types are that system's vocabulary and its notes sit on that bar's positions. A system the type vocabulary knows carries the IRI of its concept as `id`, from which the export takes the system's own context. [ontology: crm:P2 has type]"
|
|
2112
|
+
},
|
|
2110
2113
|
"versionType": {
|
|
2111
2114
|
"$ref": "#/definitions/VersionType",
|
|
2112
2115
|
"description": "Whether the version served as a master for reproductions or exists on one copy only. [ontology: crm:P2 has type]"
|
|
@@ -2127,11 +2130,30 @@
|
|
|
2127
2130
|
"@id",
|
|
2128
2131
|
"motivations",
|
|
2129
2132
|
"siglum",
|
|
2133
|
+
"system",
|
|
2130
2134
|
"@type",
|
|
2131
2135
|
"versionType"
|
|
2132
2136
|
],
|
|
2133
2137
|
"type": "object"
|
|
2134
2138
|
},
|
|
2139
|
+
"VersionCreation": {
|
|
2140
|
+
"description": "How a version was made, where that is known and worth stating.\n\nA roll issued for another system was re-punched by an editor of the publisher's, and that was an editorial act rather than a conversion: Lawson names Kähle as the man who corrected second masters for the green system. The rule he worked by is the procedure named here, and the version's edits carry it out, so the mechanical part of a transfer is stated once instead of being spelled out per note. [ontology: lrmoo:F28 Expression Creation]",
|
|
2141
|
+
"properties": {
|
|
2142
|
+
"actor": {
|
|
2143
|
+
"$ref": "#/definitions/ActorAssignment",
|
|
2144
|
+
"description": "Who carried the act out. An `ObjectAssumption`, so an attribution can carry the belief it rests on and the reasons for it. [ontology: crm:P14 carried out by]"
|
|
2145
|
+
},
|
|
2146
|
+
"date": {
|
|
2147
|
+
"$ref": "#/definitions/DateAssignment",
|
|
2148
|
+
"description": "When it took place. [ontology: dcterms:date]"
|
|
2149
|
+
},
|
|
2150
|
+
"procedure": {
|
|
2151
|
+
"$ref": "#/definitions/Concept",
|
|
2152
|
+
"description": "The rule followed, as a term of the vocabulary: the notes stand at their pitch, the expression is re-spelled in the other system's words. [ontology: crm:P33 used specific technique]"
|
|
2153
|
+
}
|
|
2154
|
+
},
|
|
2155
|
+
"type": "object"
|
|
2156
|
+
},
|
|
2135
2157
|
"VersionType": {
|
|
2136
2158
|
"description": "The type of a version. An 'edition' version may serve as the master for several roll copies; a 'unicum' version exists only on one specific copy.",
|
|
2137
2159
|
"enum": [
|
package/lib/spec/context.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { CollationTolerance, Locate } from "./Collation";
|
|
2
|
+
import { AnySymbol, Expression } from "./Symbol";
|
|
3
|
+
import { Millimeters, Track } from "./Quantity";
|
|
4
|
+
import { TrackerBar } from "./TrackerBar";
|
|
5
|
+
/**
|
|
6
|
+
* How a scale spells a command: as a perforation that turns a function
|
|
7
|
+
* on, one that cancels it, or one that holds it for as long as it lasts.
|
|
8
|
+
*/
|
|
9
|
+
export type Spelling = 'on' | 'off' | 'held';
|
|
10
|
+
/**
|
|
11
|
+
* A command as what it operates rather than as the word a scale uses
|
|
12
|
+
* for it, so that two scales can be compared at all.
|
|
13
|
+
*/
|
|
14
|
+
export interface Command {
|
|
15
|
+
/** The function operated, named the same wherever a scale has it. */
|
|
16
|
+
operates: string;
|
|
17
|
+
spelling: Spelling;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the half of the keyboard the valve serves is part of the
|
|
20
|
+
* command. The dynamics are per half; the pedals are not, and the
|
|
21
|
+
* two Welte scales put them on opposite edges of the paper, so a
|
|
22
|
+
* rule that compared sides would leave every pedal unpaired.
|
|
23
|
+
*/
|
|
24
|
+
sided: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** What a command operates, or nothing for a word no scale here knows. */
|
|
27
|
+
export declare const commandOf: (expressionType: string) => Command | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* A latched function of the older version and the held perforation of
|
|
30
|
+
* the newer one that stands for it.
|
|
31
|
+
*/
|
|
32
|
+
export interface Substitution {
|
|
33
|
+
/** The perforation that turned the function on, and the one that cancelled it. */
|
|
34
|
+
replaced: readonly [Expression, Expression];
|
|
35
|
+
/**
|
|
36
|
+
* The perforations that say the same thing in the other scale's
|
|
37
|
+
* words. Usually one, but a held command is punched as a chain of
|
|
38
|
+
* round holes with paper bridges rather than as a slot, and a scan
|
|
39
|
+
* whose analysis reports those singly gives one symbol per punch.
|
|
40
|
+
*/
|
|
41
|
+
by: readonly Expression[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* How wide a paper bridge may be for two held perforations to be one
|
|
45
|
+
* command. The T-98 punches a hold as a chain of round holes on a
|
|
46
|
+
* 2.66 mm grid with bridges of about a millimetre, so a scan read hole
|
|
47
|
+
* by hole shows a run where the paper shows one command.
|
|
48
|
+
*/
|
|
49
|
+
export declare const defaultChainGap: import("./Quantity").Quantity<"mm">;
|
|
50
|
+
/**
|
|
51
|
+
* The position a bar reads its own command for the same function on,
|
|
52
|
+
* where it has no word for this one.
|
|
53
|
+
*
|
|
54
|
+
* A version keeps the perforations it does away with in its deletions,
|
|
55
|
+
* and those may be a scale the version is not coded for: a green
|
|
56
|
+
* version deletes the red `SlowCrescendoOn` it inherits. That has no
|
|
57
|
+
* position on the green bar, so drawing it as a perforation is out of
|
|
58
|
+
* the question, but an edit still has to be shown somewhere, and the
|
|
59
|
+
* lane the green scale gives the same function is where it belongs.
|
|
60
|
+
*/
|
|
61
|
+
export declare const positionOfSameFunction: (bar: TrackerBar, symbol: Expression) => Track | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Where a held perforation of the newer version stands for a latched
|
|
64
|
+
* function of the older one: the same function, on the same side where
|
|
65
|
+
* the side counts, spanning the same stretch of the roll.
|
|
66
|
+
*
|
|
67
|
+
* Only unambiguous correspondences are reported. Where two held
|
|
68
|
+
* perforations answer one latched interval, or one answers two, none of
|
|
69
|
+
* them is reported and the editor is left to say what happened. Nothing
|
|
70
|
+
* is invented either: both sides are perforations somebody punched, and
|
|
71
|
+
* all this asserts is which stands for which.
|
|
72
|
+
*/
|
|
73
|
+
export declare const substitutionsBetween: (own: readonly AnySymbol[], inherited: readonly AnySymbol[], locate: Locate, tolerance?: CollationTolerance, chainGap?: Millimeters) => Substitution[];
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { defaultCollationTolerance } from "./Collation";
|
|
2
|
+
import { distance, mm, track } from "./Quantity";
|
|
3
|
+
/**
|
|
4
|
+
* What each Welte command operates.
|
|
5
|
+
*
|
|
6
|
+
* This cannot be read off the names. The T-100 latches a function on
|
|
7
|
+
* with one perforation and cancels it with a second, while the T-98
|
|
8
|
+
* holds it for as long as one perforation lasts (Hagmann pp. 89 f. and
|
|
9
|
+
* 100–103; Phillips p. 121), and Welte renamed two of the functions
|
|
10
|
+
* between the scales: the red `SlowCrescendo` is the green `Crescendo`,
|
|
11
|
+
* and the red's single `Forzando` valve answers to two green ones that
|
|
12
|
+
* name the end they pull towards. So the correspondence is stated here
|
|
13
|
+
* rather than derived, and only two commands that operate one function
|
|
14
|
+
* can stand for each other.
|
|
15
|
+
*
|
|
16
|
+
* The T-100's `MotorOn`/`MotorOff`, `Rewind` and `ElectricCutOff` are
|
|
17
|
+
* left out on purpose: the green scale has no word for any of them, its
|
|
18
|
+
* motor switch being an automatic mercury contact (Skala-Rolle 98 §12)
|
|
19
|
+
* and its rewind riding on the bass sforzando-piano line. A red command
|
|
20
|
+
* of those kinds has no counterpart and stays a plain deletion.
|
|
21
|
+
*/
|
|
22
|
+
const commands = {
|
|
23
|
+
MezzoforteOn: { operates: 'mezzoforte', spelling: 'on', sided: true },
|
|
24
|
+
MezzoforteOff: { operates: 'mezzoforte', spelling: 'off', sided: true },
|
|
25
|
+
SlowCrescendoOn: { operates: 'crescendo', spelling: 'on', sided: true },
|
|
26
|
+
SlowCrescendoOff: { operates: 'crescendo', spelling: 'off', sided: true },
|
|
27
|
+
ForzandoOn: { operates: 'sforzando', spelling: 'on', sided: true },
|
|
28
|
+
ForzandoOff: { operates: 'sforzando', spelling: 'off', sided: true },
|
|
29
|
+
SustainPedalOn: { operates: 'sustainPedal', spelling: 'on', sided: false },
|
|
30
|
+
SustainPedalOff: { operates: 'sustainPedal', spelling: 'off', sided: false },
|
|
31
|
+
SoftPedalOn: { operates: 'softPedal', spelling: 'on', sided: false },
|
|
32
|
+
SoftPedalOff: { operates: 'softPedal', spelling: 'off', sided: false },
|
|
33
|
+
Mezzoforte: { operates: 'mezzoforte', spelling: 'held', sided: true },
|
|
34
|
+
Crescendo: { operates: 'crescendo', spelling: 'held', sided: true },
|
|
35
|
+
SforzandoForte: { operates: 'sforzando', spelling: 'held', sided: true },
|
|
36
|
+
SforzandoPiano: { operates: 'sforzando', spelling: 'held', sided: true },
|
|
37
|
+
SustainPedal: { operates: 'sustainPedal', spelling: 'held', sided: false },
|
|
38
|
+
SoftPedal: { operates: 'softPedal', spelling: 'held', sided: false }
|
|
39
|
+
};
|
|
40
|
+
/** What a command operates, or nothing for a word no scale here knows. */
|
|
41
|
+
export const commandOf = (expressionType) => commands[expressionType];
|
|
42
|
+
/**
|
|
43
|
+
* How wide a paper bridge may be for two held perforations to be one
|
|
44
|
+
* command. The T-98 punches a hold as a chain of round holes on a
|
|
45
|
+
* 2.66 mm grid with bridges of about a millimetre, so a scan read hole
|
|
46
|
+
* by hole shows a run where the paper shows one command.
|
|
47
|
+
*/
|
|
48
|
+
export const defaultChainGap = mm(3);
|
|
49
|
+
const isExpression = (symbol) => symbol.type === 'expression';
|
|
50
|
+
/** The key a command is matched on: the function, and the side where the side counts. */
|
|
51
|
+
const functionKey = (symbol, command) => command.sided ? `${command.operates} ${symbol.scope}` : command.operates;
|
|
52
|
+
/**
|
|
53
|
+
* The position a bar reads its own command for the same function on,
|
|
54
|
+
* where it has no word for this one.
|
|
55
|
+
*
|
|
56
|
+
* A version keeps the perforations it does away with in its deletions,
|
|
57
|
+
* and those may be a scale the version is not coded for: a green
|
|
58
|
+
* version deletes the red `SlowCrescendoOn` it inherits. That has no
|
|
59
|
+
* position on the green bar, so drawing it as a perforation is out of
|
|
60
|
+
* the question, but an edit still has to be shown somewhere, and the
|
|
61
|
+
* lane the green scale gives the same function is where it belongs.
|
|
62
|
+
*/
|
|
63
|
+
export const positionOfSameFunction = (bar, symbol) => {
|
|
64
|
+
const wanted = commandOf(symbol.expressionType);
|
|
65
|
+
if (!wanted)
|
|
66
|
+
return undefined;
|
|
67
|
+
const positions = Array.from({ length: bar.trackCount }, (_, index) => track(index + 1));
|
|
68
|
+
return positions.find(position => {
|
|
69
|
+
const meaning = bar.meaningOf(position);
|
|
70
|
+
if (meaning?.type !== 'expression')
|
|
71
|
+
return false;
|
|
72
|
+
const command = commandOf(meaning.expressionType);
|
|
73
|
+
return command !== undefined
|
|
74
|
+
&& command.operates === wanted.operates
|
|
75
|
+
&& functionKey(meaning, command) === functionKey(symbol, wanted);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The intervals the older version latches: each perforation that turns
|
|
80
|
+
* a function on, with the one that cancels it next on the same line.
|
|
81
|
+
*/
|
|
82
|
+
const latchedIn = (symbols, locate) => {
|
|
83
|
+
const byFunction = new Map();
|
|
84
|
+
symbols.filter(isExpression).forEach(symbol => {
|
|
85
|
+
const command = commandOf(symbol.expressionType);
|
|
86
|
+
const at = locate(symbol)?.from;
|
|
87
|
+
if (!command || command.spelling === 'held' || at === undefined)
|
|
88
|
+
return;
|
|
89
|
+
const key = functionKey(symbol, command);
|
|
90
|
+
const group = byFunction.get(key) ?? [];
|
|
91
|
+
group.push({ symbol, at, spelling: command.spelling });
|
|
92
|
+
byFunction.set(key, group);
|
|
93
|
+
});
|
|
94
|
+
return [...byFunction.values()].flatMap(group => {
|
|
95
|
+
const inOrder = [...group].sort((a, b) => a.at - b.at);
|
|
96
|
+
return inOrder.reduce((state, entry) => {
|
|
97
|
+
// A second On while the function already stands is
|
|
98
|
+
// redundant, and the interval began at the first, which
|
|
99
|
+
// is how a red copy reads that reaches the end of the
|
|
100
|
+
// roll with two Ons and one Off.
|
|
101
|
+
if (entry.spelling === 'on') {
|
|
102
|
+
return state.open ? state : { ...state, open: { on: entry.symbol, from: entry.at } };
|
|
103
|
+
}
|
|
104
|
+
return state.open
|
|
105
|
+
? {
|
|
106
|
+
open: undefined,
|
|
107
|
+
closed: [...state.closed, {
|
|
108
|
+
on: state.open.on,
|
|
109
|
+
off: entry.symbol,
|
|
110
|
+
from: state.open.from,
|
|
111
|
+
to: entry.at,
|
|
112
|
+
key: functionKey(state.open.on, commandOf(state.open.on.expressionType))
|
|
113
|
+
}]
|
|
114
|
+
}
|
|
115
|
+
: state;
|
|
116
|
+
}, { closed: [] }).closed;
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* The commands the newer version holds, a chain of punches counting as
|
|
121
|
+
* the one command the paper shows rather than as a run of them.
|
|
122
|
+
*/
|
|
123
|
+
const heldRunsIn = (symbols, locate, chainGap) => {
|
|
124
|
+
const byFunction = new Map();
|
|
125
|
+
symbols.filter(isExpression).forEach(symbol => {
|
|
126
|
+
const command = commandOf(symbol.expressionType);
|
|
127
|
+
const place = locate(symbol);
|
|
128
|
+
if (!command || command.spelling !== 'held' || !place)
|
|
129
|
+
return;
|
|
130
|
+
const key = functionKey(symbol, command);
|
|
131
|
+
const group = byFunction.get(key) ?? [];
|
|
132
|
+
group.push({ symbol, from: place.from, to: place.to });
|
|
133
|
+
byFunction.set(key, group);
|
|
134
|
+
});
|
|
135
|
+
return [...byFunction.entries()].flatMap(([key, group]) => [...group]
|
|
136
|
+
.sort((a, b) => a.from - b.from)
|
|
137
|
+
.reduce((runs, punch) => {
|
|
138
|
+
const open = runs[runs.length - 1];
|
|
139
|
+
if (open && punch.from - open.to <= chainGap) {
|
|
140
|
+
open.of.push(punch.symbol);
|
|
141
|
+
open.to = punch.to > open.to ? punch.to : open.to;
|
|
142
|
+
return runs;
|
|
143
|
+
}
|
|
144
|
+
return [...runs, { of: [punch.symbol], from: punch.from, to: punch.to, key }];
|
|
145
|
+
}, []));
|
|
146
|
+
};
|
|
147
|
+
/** The only item of the list, or nothing where there is none or a rival. */
|
|
148
|
+
const theOnly = (items) => items.length === 1 ? items[0] : undefined;
|
|
149
|
+
/**
|
|
150
|
+
* Where a held perforation of the newer version stands for a latched
|
|
151
|
+
* function of the older one: the same function, on the same side where
|
|
152
|
+
* the side counts, spanning the same stretch of the roll.
|
|
153
|
+
*
|
|
154
|
+
* Only unambiguous correspondences are reported. Where two held
|
|
155
|
+
* perforations answer one latched interval, or one answers two, none of
|
|
156
|
+
* them is reported and the editor is left to say what happened. Nothing
|
|
157
|
+
* is invented either: both sides are perforations somebody punched, and
|
|
158
|
+
* all this asserts is which stands for which.
|
|
159
|
+
*/
|
|
160
|
+
export const substitutionsBetween = (own, inherited, locate, tolerance = defaultCollationTolerance, chainGap = defaultChainGap) => {
|
|
161
|
+
const latched = latchedIn(inherited, locate);
|
|
162
|
+
const spans = (run, interval) => distance(run.from, interval.from) <= tolerance.toleranceStart
|
|
163
|
+
&& distance(run.to, interval.to) <= tolerance.toleranceEnd;
|
|
164
|
+
const candidates = heldRunsIn(own, locate, chainGap).flatMap(run => {
|
|
165
|
+
const answered = latched.filter(interval => interval.key === run.key && spans(run, interval));
|
|
166
|
+
const only = theOnly(answered);
|
|
167
|
+
return only ? [{ run, interval: only }] : [];
|
|
168
|
+
});
|
|
169
|
+
// A latched interval two runs both answer is ambiguous from its side
|
|
170
|
+
// as well, so neither of them is reported.
|
|
171
|
+
const claims = candidates.reduce((counts, { interval }) => counts.set(interval.on.id, (counts.get(interval.on.id) ?? 0) + 1), new Map());
|
|
172
|
+
return candidates
|
|
173
|
+
.filter(({ interval }) => claims.get(interval.on.id) === 1)
|
|
174
|
+
.map(({ run, interval }) => ({ replaced: [interval.on, interval.off], by: run.of }));
|
|
175
|
+
};
|
package/package.json
CHANGED