linked-rolls 0.36.0 → 0.37.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/editionOps.js CHANGED
@@ -4,7 +4,7 @@ import { getAt } from "./EditionView";
4
4
  import { isPerforation, placementRelations } from "./Symbol";
5
5
  import { collationsOf, defaultCollationTolerance } from "./Collation";
6
6
  import { collationToleranceOf, editsOf, insertedBy, principalDerivationOf } from "./Version";
7
- import { asSymbols, barOf, isPaperStretch } from "./RollCopy";
7
+ import { asSymbols, barOf, featuresByAct, featuresOf, isPaperStretch, statesNothing } from "./RollCopy";
8
8
  import { systemOf } from "./TrackerBar";
9
9
  import { substitutionsBetween } from "./substitution";
10
10
  import { trackerBarOf } from "./systems";
@@ -24,7 +24,7 @@ const onVersion = (versionId, op) => draft => {
24
24
  op(version, draft);
25
25
  };
26
26
  const onFeature = (copyId, featureId, op) => onCopy(copyId, copy => {
27
- const feature = copy.features.find(f => f.id === featureId);
27
+ const feature = featuresOf(copy).find(f => f.id === featureId);
28
28
  if (feature)
29
29
  op(feature);
30
30
  });
@@ -82,7 +82,7 @@ export const createVersion = (copy) => draft => {
82
82
  type: 'Version',
83
83
  id: v4(),
84
84
  system: systemOf(bar),
85
- edits: asSymbols(copy.features, bar).map(insertion),
85
+ edits: asSymbols(featuresOf(copy), bar).map(insertion),
86
86
  motivations: []
87
87
  });
88
88
  };
@@ -203,7 +203,7 @@ export const stateFeatureCondition = (copyId, featureId, condition) => onFeature
203
203
  feature.condition = condition;
204
204
  });
205
205
  /** The ids of every feature the copy bears, those a patch bears among them. */
206
- const featureIdsOf = (copy) => new Set(copy.features.flatMap(withBorneFeatures).map(feature => feature.id));
206
+ const featureIdsOf = (copy) => new Set(featuresOf(copy).flatMap(withBorneFeatures).map(feature => feature.id));
207
207
  /**
208
208
  * A symbol every carrier of which lies among the features loses its
209
209
  * evidence with them. A symbol without carriers, such as a label,
@@ -279,26 +279,27 @@ const writeInto = (draft, before, after) => Object.entries(after).forEach(([key,
279
279
  const isCopy = (record) => 'type' in record && record.type === 'RollCopy';
280
280
  const isBelief = (record) => 'type' in record && record.type === 'belief';
281
281
  const isComprehension = (reason) => reason.type === 'meaningComprehension';
282
- /** What the modification names, be it as added or as removed. */
283
- const membersOf = (modification) => modification.type === 'Addition' ? modification.added : modification.removed;
284
- const namesNothing = (modification) => membersOf(modification).length === 0;
285
282
  const comprehendsNothing = (reason) => isComprehension(reason) && reason.comprehends.length === 0;
286
- /** The modification with what it names rewritten, or the very same one where that leaves it as it was. */
287
- const renamingMembers = (rename) => (modification) => modification.type === 'Addition'
288
- ? replacing(modification, 'added', rename(modification.added))
289
- : replacing(modification, 'removed', rename(modification.removed));
283
+ /**
284
+ * The modification with what it names rewritten, or the very same one
285
+ * where that leaves it as it was. Only a removal names anything by id:
286
+ * what an act produced or glued on it states itself.
287
+ */
288
+ const renamingMembers = (rename) => (modification) => modification.type === 'Removal'
289
+ ? replacing(modification, 'removed', rename(modification.removed))
290
+ : modification;
290
291
  /** The reason with what a comprehension comprehends rewritten; any other reason names nothing of the kind. */
291
292
  const renamingComprehended = (rename) => (reason) => isComprehension(reason) ? replacing(reason, 'comprehends', rename(reason.comprehends)) : reason;
292
293
  /**
293
- * The record with the ids it names rewritten: what a copy's
294
- * modifications added or removed, and what the comprehensions among a
295
- * belief's reasons comprehend. A modification or a comprehension the
296
- * rewriting emptied goes with what it named; one that named nothing
297
- * before stays, as an edit that was empty before it does.
294
+ * The record with the ids it names rewritten: what a copy's removals
295
+ * removed, and what the comprehensions among a belief's reasons
296
+ * comprehend. A removal or a comprehension the rewriting emptied goes
297
+ * with what it named; one that named nothing before stays, as an edit
298
+ * that was empty before it does.
298
299
  */
299
300
  const renaming = (rename) => (record) => {
300
301
  if (isCopy(record)) {
301
- return replacing(record, 'modifications', pruned(record.modifications, renamingMembers(rename), namesNothing));
302
+ return replacing(record, 'modifications', pruned(record.modifications, renamingMembers(rename), statesNothing));
302
303
  }
303
304
  if (isBelief(record)) {
304
305
  return replacing(record, 'reasons', pruned(record.reasons, renamingComprehended(rename), comprehendsNothing));
@@ -356,16 +357,39 @@ const withoutBorne = (feature, named) => {
356
357
  };
357
358
  /** The features without those named, wherever they lie, and without whatever those bore. */
358
359
  const withoutFeatures = (features, named) => mapped(without(features, feature => named.has(feature.id)), feature => withoutBorne(feature, named));
360
+ /** The act with its features rewritten, or the very same one where the rewriting left them. */
361
+ const rewritingAct = (rewrite) => (modification) => {
362
+ if (modification.type === 'Alteration')
363
+ return replacing(modification, 'produced', rewrite(modification.produced));
364
+ if (modification.type === 'Attachment')
365
+ return replacing(modification, 'added', rewrite(modification.added));
366
+ return modification;
367
+ };
368
+ /**
369
+ * Writes the rewriting onto the features of every act of the copy, and
370
+ * takes out an act it left with nothing to state.
371
+ */
372
+ const rewriteFeatures = (copy, rewrite) => {
373
+ const state = stateOf(copy);
374
+ const produced = state.production?.produced;
375
+ if (produced && copy.production) {
376
+ const rewritten = rewrite(produced);
377
+ if (rewritten !== produced)
378
+ copy.production.produced = rewritten;
379
+ }
380
+ copy.modifications = pruned(state.modifications, rewritingAct(rewrite), statesNothing);
381
+ };
359
382
  /**
360
383
  * Takes the features off the copy, and out of the versions with what
361
384
  * only they carried. A feature may be named wherever the copy bears it:
362
385
  * a patch goes with everything glued onto it, and a feature of a patch
363
- * may be taken back on its own, the patch staying where it is.
386
+ * may be taken back on its own, the patch staying where it is. An act
387
+ * left having produced or added nothing goes as well.
364
388
  */
365
389
  export const removeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
366
390
  const named = new Set(featureIds);
367
- const features = stateOf(copy.features);
368
- copy.features = withoutFeatures(features, named);
391
+ const features = featuresOf(stateOf(copy));
392
+ rewriteFeatures(copy, (stated) => withoutFeatures(stated, named));
369
393
  forgetFeatures(draft, new Set(goneWith(features, named)));
370
394
  });
371
395
  /**
@@ -445,9 +469,9 @@ const spanning = (features) => ({
445
469
  * others being alike in all but their place. The depiction goes: a
446
470
  * region of the scan showing one part depicts no more than that part.
447
471
  */
448
- const mergedFrom = (features) => {
472
+ const mergedFrom = (features, id) => {
449
473
  const stating = features.find(feature => feature.condition) ?? features[0];
450
- const merged = { ...stating, id: v4(), horizontal: spanning(features) };
474
+ const merged = { ...stating, id, horizontal: spanning(features) };
451
475
  delete merged.depiction;
452
476
  return merged;
453
477
  };
@@ -485,24 +509,29 @@ const carryOver = (draft, replaced, mergedId) => {
485
509
  * Only the features the copy bears itself are merged. A feature of a
486
510
  * patch states no place along the roll of its own, so there is nothing
487
511
  * for a merge to span, and an id naming one is passed over as an id the
488
- * copy does not bear is.
512
+ * copy does not bear is. Features of two different acts are not merged
513
+ * either: one feature is the work of one act.
489
514
  *
490
515
  * Throws where the features cannot stand for one; `mergeObstacle`
491
516
  * says beforehand whether they can.
492
517
  */
493
518
  export const mergeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
494
519
  const named = new Set(featureIds);
495
- const features = stateOf(copy.features);
496
- const toMerge = features.filter(feature => named.has(feature.id));
497
- const obstacle = mergeObstacle(toMerge);
520
+ const holds = (features) => features.some(feature => named.has(feature.id));
521
+ const acts = featuresByAct(stateOf(copy)).filter(holds);
522
+ const toMerge = (acts[0] ?? []).filter(feature => named.has(feature.id));
523
+ const obstacle = acts.length > 1 ? 'different-acts' : mergeObstacle(toMerge);
498
524
  if (obstacle) {
499
525
  throw new Error(`The features of copy ${copyId} cannot be merged: ${obstacle}`);
500
526
  }
501
527
  const replaced = new Set(toMerge.map(feature => feature.id));
502
- const merged = mergedFrom(toMerge);
503
- copy.features = standingFor(features, feature => replaced.has(feature.id), merged);
504
- carryOver(draft, replaced, merged.id);
505
- renameReferences(draft, ids => standingFor(ids, id => replaced.has(id), merged.id));
528
+ const mergedId = v4();
529
+ rewriteFeatures(copy, (stated) => {
530
+ const stands = (feature) => replaced.has(feature.id);
531
+ return stated.some(stands) ? standingFor(stated, stands, mergedFrom(stated.filter(stands), mergedId)) : stated;
532
+ });
533
+ carryOver(draft, replaced, mergedId);
534
+ renameReferences(draft, ids => standingFor(ids, id => replaced.has(id), mergedId));
506
535
  });
507
536
  /** The carriers of each collated symbol pass to its counterpart. */
508
537
  const handOverCarriers = (view, draft, collations) => collations.forEach(({ symbol, counterpart }) => {
@@ -1,3 +1,4 @@
1
+ import { derivedKeys } from "./asJsonLd";
1
2
  import { migrate } from "./migrate";
2
3
  import { isDateString } from "./utils";
3
4
  export const importDate = (str) => {
@@ -17,6 +18,15 @@ const fromJsonLdValue = (value) => {
17
18
  return fromJsonLdEntity(value);
18
19
  return value;
19
20
  };
21
+ /**
22
+ * A node without what the export derived from it. A copy bears the
23
+ * features its acts brought about, and the graph has to be told so.
24
+ * In the edition the act that made a feature is the one place it
25
+ * stands.
26
+ */
27
+ const asStated = (json) => Object.keys(json).some(key => derivedKeys.has(key))
28
+ ? Object.fromEntries(Object.entries(json).filter(([key]) => !derivedKeys.has(key)))
29
+ : json;
20
30
  /**
21
31
  * An entity with its keywords read as plain keys. The input is left as
22
32
  * it is. The `@type` of a value object names the datatype of the value,
@@ -25,7 +35,7 @@ const fromJsonLdValue = (value) => {
25
35
  * rather than to the edition, which states the system as data.
26
36
  */
27
37
  const fromJsonLdEntity = (json) => {
28
- const { '@type': type, '@id': id, '@context': context, ...rest } = json;
38
+ const { '@type': type, '@id': id, '@context': context, ...rest } = asStated(json);
29
39
  const entity = Object.fromEntries(Object.entries(rest).map(([key, value]) => [key, fromJsonLdValue(value)]));
30
40
  if (type !== undefined && !('@value' in json))
31
41
  entity.type = type;
package/lib/migrate.js CHANGED
@@ -1,4 +1,4 @@
1
- import { conditions } from "./Feature";
1
+ import { conditions, isFeatureType } from "./Feature";
2
2
  import { rollConditions } from "./RollCopy";
3
3
  import { systemIdIn, systemOf, translationBetween } from "./TrackerBar";
4
4
  import { trackerBars } from "./systems";
@@ -12,6 +12,14 @@ const renamedKeys = {
12
12
  classification: 'editType'
13
13
  };
14
14
  const referenceKeys = ['alignedWith', 'pairedWith', 'basedOn'];
15
+ /**
16
+ * The five type terms that were capitalised while the rest of the
17
+ * vocabulary was not, under the keys that carry them.
18
+ */
19
+ const lowerCasedTerms = {
20
+ method: { Print: 'print', Handwriting: 'handwriting', Stamp: 'stamp' },
21
+ material: { Paper: 'paper', Tape: 'tape' }
22
+ };
15
23
  const named = (name) => ({ name, sameAs: [] });
16
24
  const withRenamedKeys = (node) => Object.keys(node).some(key => Object.hasOwn(renamedKeys, key))
17
25
  ? Object.fromEntries(Object.entries(node).map(([key, value]) => [renamedKeys[key] ?? key, value]))
@@ -32,6 +40,10 @@ const withoutVersionType = (node) => {
32
40
  const { versionType: _retired, ...rest } = node;
33
41
  return rest;
34
42
  };
43
+ const withLowerCaseTerms = (node) => Object.entries(lowerCasedTerms).reduce((result, [key, terms]) => {
44
+ const lowered = terms[result[key]];
45
+ return lowered ? { ...result, [key]: lowered } : result;
46
+ }, node);
35
47
  const withReferences = (node) => referenceKeys.reduce((result, key) => {
36
48
  const reference = result[key];
37
49
  if (reference && typeof reference === 'object' && '@value' in reference) {
@@ -79,15 +91,100 @@ const withScale = (node) => {
79
91
  /** A derivation written as a single one, before a version could name several. */
80
92
  const isSingleDerivation = (basedOn) => basedOn !== null && typeof basedOn === 'object' && !Array.isArray(basedOn);
81
93
  const withDerivationList = (node) => isSingleDerivation(node.basedOn) ? { ...node, basedOn: [node.basedOn] } : node;
94
+ /** The features a copy states at a place of its own, in either shape. */
95
+ const featuresOf = (copy) => [
96
+ ...(Array.isArray(copy.features) ? copy.features : []),
97
+ ...(Array.isArray(copy.production?.produced) ? copy.production.produced : [])
98
+ ];
82
99
  /**
83
100
  * A copy read on a roll reader was stated as a recording before a sound
84
101
  * recording could be one. A sound recording gives no holes, so a recording
85
102
  * with holes is a reading.
86
103
  */
87
- const withReadingKind = (node) => node.readFrom?.kind === 'recording' && Array.isArray(node.features)
88
- && node.features.some((feature) => feature?.['@type'] === 'Hole')
104
+ const withReadingKind = (node) => node.readFrom?.kind === 'recording'
105
+ && featuresOf(node).some((feature) => feature?.['@type'] === 'Hole')
89
106
  ? { ...node, readFrom: { ...node.readFrom, kind: 'reading' } }
90
107
  : node;
108
+ /** A feature written while the kind of a feature was a type beside its class. */
109
+ const withoutFeatureKind = (node) => {
110
+ if (!isFeatureType(node['@type']) || !Object.hasOwn(node, 'kind'))
111
+ return node;
112
+ const { kind: _typed, ...rest } = node;
113
+ return rest;
114
+ };
115
+ const isPatch = (feature) => feature?.['@type'] === 'GluedOn';
116
+ /**
117
+ * A feature a patch bears was sometimes written without an id, and what
118
+ * has no id can be referred to from nowhere: neither by a reading of it
119
+ * nor by the statement that the copy bears it. It is named after the
120
+ * patch and the place it stands in.
121
+ */
122
+ const withBorneFeaturesNamed = (node) => {
123
+ const bearer = node['@id'];
124
+ if (typeof bearer !== 'string' || !Array.isArray(node.features))
125
+ return node;
126
+ if (node.features.every((feature) => typeof feature?.['@id'] === 'string'))
127
+ return node;
128
+ return {
129
+ ...node,
130
+ features: node.features.map((feature, index) => feature && typeof feature === 'object' && typeof feature['@id'] !== 'string'
131
+ ? { '@id': `${bearer}-${index + 1}`, ...feature }
132
+ : feature)
133
+ };
134
+ };
135
+ /**
136
+ * A copy held its features in one list and a modification named by id
137
+ * what it had added. Each feature now stands in the act that brought it
138
+ * about: a patch in an attachment, a feature an act names in an
139
+ * alteration, and everything else in the production of the copy, which
140
+ * is the punching. An addition naming both a patch and a feature
141
+ * becomes two acts, since gluing something on and drawing something are
142
+ * not one act.
143
+ *
144
+ * A feature a patch bears is not moved: it has no place of its own, and
145
+ * it came onto the copy with the patch.
146
+ */
147
+ const withFeaturesInActs = (node) => {
148
+ if (node['@type'] !== 'RollCopy' || !Array.isArray(node.features))
149
+ return node;
150
+ const { features, modifications, ...rest } = node;
151
+ const byId = new Map(features
152
+ .filter((feature) => typeof feature?.['@id'] === 'string')
153
+ .map((feature) => [feature['@id'], feature]));
154
+ const taken = new Set();
155
+ /** The features the modification named, each going to the first act that names it. */
156
+ const namedBy = (modification) => (Array.isArray(modification.added) ? modification.added : [])
157
+ .flatMap((id) => {
158
+ if (typeof id !== 'string' || taken.has(id) || !byId.has(id))
159
+ return [];
160
+ taken.add(id);
161
+ return [byId.get(id)];
162
+ });
163
+ const acts = (Array.isArray(modifications) ? modifications : []).flatMap((modification) => {
164
+ if (modification?.['@type'] !== 'Addition')
165
+ return [modification];
166
+ const { added: _named, ...act } = modification;
167
+ const held = namedBy(modification);
168
+ const glued = held.filter(isPatch);
169
+ const made = held.filter(feature => !isPatch(feature));
170
+ if (glued.length === 0)
171
+ return [{ ...act, '@type': 'Alteration', produced: made }];
172
+ return [
173
+ ...(made.length > 0 ? [{ ...act, '@type': 'Alteration', produced: made }] : []),
174
+ { ...act, '@type': 'Attachment', added: glued }
175
+ ];
176
+ });
177
+ const left = features.filter((feature) => !taken.has(feature?.['@id']));
178
+ const punched = left.filter((feature) => !isPatch(feature));
179
+ const loose = left.filter(isPatch);
180
+ return {
181
+ ...rest,
182
+ ...((rest.production || punched.length > 0) && {
183
+ production: { ...rest.production, ...(punched.length > 0 && { produced: punched }) }
184
+ }),
185
+ modifications: [...acts, ...(loose.length > 0 ? [{ '@type': 'Attachment', added: loose }] : [])]
186
+ };
187
+ };
91
188
  /** A keeper nobody could name was written as an empty one before a copy could leave it out. */
92
189
  const withoutEmptyKeeper = (node) => {
93
190
  if (node.keeper?.name !== '' || node.keeper.sameAs?.length)
@@ -106,8 +203,9 @@ const withTimeSpanDates = (node) => {
106
203
  const { '@value': within, '@type': _typed, ...rest } = node;
107
204
  return { ...rest, within };
108
205
  };
109
- const migrateNode = (node) => [withRenamedKeys, withTypology, withoutVersionType, withReferences, withKeeper, withoutEmptyKeeper, withProductionNodes, withScale,
110
- withDerivationList, withReadingKind, withTimeSpanDates]
206
+ const migrateNode = (node) => [withRenamedKeys, withTypology, withoutVersionType, withLowerCaseTerms, withReferences, withKeeper, withoutEmptyKeeper,
207
+ withProductionNodes, withScale, withDerivationList, withReadingKind, withTimeSpanDates, withoutFeatureKind,
208
+ withBorneFeaturesNamed, withFeaturesInActs]
111
209
  .reduce((result, step) => step(result), node);
112
210
  /** The items each walked, or the very same list where the walk changed none. */
113
211
  const walked = (items) => {
@@ -148,9 +148,8 @@ export function readFromPhillipsEroll(buffer, { system = welteT100, controlOffse
148
148
  ops: [],
149
149
  conditions: [],
150
150
  measurements: {},
151
- production: { system: systemOf(system) },
151
+ production: { system: systemOf(system), produced: features },
152
152
  modifications: [],
153
- features,
154
153
  readFrom: {
155
154
  kind: 'reading',
156
155
  actor: assignObject({ name: 'Phillips, Peter', sameAs: [] }),
@@ -114,9 +114,8 @@ export function readFromSpencerBar(buffer, { rowsPerInch = SPENCER_ROWS_PER_INCH
114
114
  ops: [],
115
115
  conditions: [],
116
116
  measurements: {},
117
- production: { system: systemOf(system) },
118
- modifications: [],
119
- features
117
+ production: { system: systemOf(system), produced: features },
118
+ modifications: []
120
119
  };
121
120
  }
122
121
  /**
@@ -157,7 +157,7 @@ export function readFromStanfordAton(atonString, { trackShift, system = welteT10
157
157
  id: v4(),
158
158
  ops: [],
159
159
  conditions: [],
160
- production: { system: systemOf(system) },
160
+ production: { system: systemOf(system), produced: features },
161
161
  modifications: [],
162
162
  ...((scan ?? stanford) && { scan: scan ?? stanford?.scan }),
163
163
  measurements: {
@@ -183,7 +183,6 @@ export function readFromStanfordAton(atonString, { trackShift, system = welteT10
183
183
  }),
184
184
  trackCalibration: calibration,
185
185
  ...(measuredBy && { measuredBy })
186
- },
187
- features
186
+ }
188
187
  };
189
188
  }
@@ -1,5 +1,5 @@
1
1
  import { bearsPhysicalEvidence, isMeasured, sourceLabels } from "./FeatureSource";
2
- import { calibrationOf } from "./RollCopy";
2
+ import { calibrationOf, featuresOf } from "./RollCopy";
3
3
  import { trackerBarOf } from "./systems";
4
4
  import { witnessesOf } from "./witnesses";
5
5
  export const reservationTypes = [
@@ -81,7 +81,7 @@ const keeperKnown = copy => copy.keeper ? undefined : {
81
81
  note: 'Nobody is known to hold the copy, so its paper cannot be consulted.'
82
82
  };
83
83
  /** A check on what a copy's features leave open, which has nothing to say of a copy without any. */
84
- const onFeatures = (check) => copy => copy.features.length > 0 ? check(copy) : undefined;
84
+ const onFeatures = (check) => copy => featuresOf(copy).length > 0 ? check(copy) : undefined;
85
85
  const checks = [
86
86
  sourceStated,
87
87
  sourceDocumented,