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.
Files changed (131) hide show
  1. package/README.md +54 -361
  2. package/lib/{constraints.d.ts → analysis/constraints.d.ts} +1 -1
  3. package/lib/{constraints.js → analysis/constraints.js} +9 -8
  4. package/lib/analysis/ownPaper.d.ts +26 -0
  5. package/lib/analysis/ownPaper.js +41 -0
  6. package/lib/{reservations.d.ts → analysis/reservations.d.ts} +3 -3
  7. package/lib/{reservations.js → analysis/reservations.js} +3 -3
  8. package/lib/{sigla.d.ts → analysis/sigla.d.ts} +2 -2
  9. package/lib/{sigla.js → analysis/sigla.js} +4 -4
  10. package/lib/{witnesses.d.ts → analysis/witnesses.d.ts} +2 -2
  11. package/lib/{witnesses.js → analysis/witnesses.js} +4 -4
  12. package/lib/{Collation.d.ts → collation/Collation.d.ts} +4 -4
  13. package/lib/{Collation.js → collation/Collation.js} +4 -4
  14. package/lib/{alignment.d.ts → collation/alignment.d.ts} +4 -4
  15. package/lib/{alignment.js → collation/alignment.js} +10 -16
  16. package/lib/{scatter.d.ts → collation/scatter.d.ts} +4 -4
  17. package/lib/{scatter.js → collation/scatter.js} +6 -6
  18. package/lib/{statistics.d.ts → collation/statistics.d.ts} +1 -1
  19. package/lib/{statistics.js → collation/statistics.js} +2 -2
  20. package/lib/{substitution.d.ts → collation/substitution.d.ts} +9 -24
  21. package/lib/{substitution.js → collation/substitution.js} +13 -37
  22. package/lib/emulation/Emulation.d.ts +64 -0
  23. package/lib/emulation/Emulation.js +282 -0
  24. package/lib/emulation/negotiation.d.ts +14 -0
  25. package/lib/emulation/negotiation.js +20 -0
  26. package/lib/index.d.ts +43 -35
  27. package/lib/index.js +36 -35
  28. package/lib/{asJsonLd.d.ts → io/asJsonLd.d.ts} +1 -1
  29. package/lib/{asJsonLd.js → io/asJsonLd.js} +22 -51
  30. package/lib/{context.d.ts → io/context.d.ts} +4 -4
  31. package/lib/{context.js → io/context.js} +4 -4
  32. package/lib/{importJsonLd.d.ts → io/importJsonLd.d.ts} +1 -1
  33. package/lib/io/importJsonLd.js +128 -0
  34. package/lib/{migrate.d.ts → io/migrate.d.ts} +9 -0
  35. package/lib/{migrate.js → io/migrate.js} +97 -54
  36. package/lib/{Agent.d.ts → model/Agent.d.ts} +1 -1
  37. package/lib/{Assumption.d.ts → model/Assumption.d.ts} +1 -1
  38. package/lib/{ConditionState.d.ts → model/ConditionState.d.ts} +1 -2
  39. package/lib/{Edit.d.ts → model/Edit.d.ts} +1 -1
  40. package/lib/{Edition.d.ts → model/Edition.d.ts} +2 -2
  41. package/lib/{Feature.d.ts → model/Feature.d.ts} +3 -1
  42. package/lib/{Feature.js → model/Feature.js} +2 -0
  43. package/lib/{FeatureSource.d.ts → model/FeatureSource.d.ts} +1 -1
  44. package/lib/{Perforator.d.ts → model/Perforator.d.ts} +1 -1
  45. package/lib/{RollCopy.d.ts → model/RollCopy.d.ts} +5 -12
  46. package/lib/{RollCopy.js → model/RollCopy.js} +3 -5
  47. package/lib/{Symbol.d.ts → model/Symbol.d.ts} +1 -1
  48. package/lib/{Version.d.ts → model/Version.d.ts} +11 -3
  49. package/lib/{Version.js → model/Version.js} +24 -1
  50. package/lib/{vocabulary.js → model/vocabulary.js} +2 -2
  51. package/lib/ops/beliefs.d.ts +9 -0
  52. package/lib/ops/beliefs.js +31 -0
  53. package/lib/ops/collation.d.ts +62 -0
  54. package/lib/ops/collation.js +228 -0
  55. package/lib/ops/commands.d.ts +9 -0
  56. package/lib/ops/commands.js +29 -0
  57. package/lib/ops/copies.d.ts +74 -0
  58. package/lib/ops/copies.js +135 -0
  59. package/lib/ops/draft.d.ts +59 -0
  60. package/lib/ops/draft.js +81 -0
  61. package/lib/ops/edits.d.ts +10 -0
  62. package/lib/ops/edits.js +82 -0
  63. package/lib/ops/features.d.ts +84 -0
  64. package/lib/ops/features.js +296 -0
  65. package/lib/ops/forget.d.ts +31 -0
  66. package/lib/ops/forget.js +110 -0
  67. package/lib/ops/immutable.d.ts +16 -0
  68. package/lib/ops/immutable.js +46 -0
  69. package/lib/ops/index.d.ts +13 -0
  70. package/lib/ops/index.js +7 -0
  71. package/lib/ops/versions.d.ts +33 -0
  72. package/lib/ops/versions.js +95 -0
  73. package/lib/readers/AtonParser.js +5 -7
  74. package/lib/readers/phillipsEroll.d.ts +3 -3
  75. package/lib/readers/phillipsEroll.js +4 -5
  76. package/lib/readers/spencerBar.d.ts +2 -2
  77. package/lib/readers/spencerBar.js +2 -3
  78. package/lib/readers/stanfordAton.d.ts +3 -3
  79. package/lib/readers/stanfordAton.js +2 -4
  80. package/lib/schema.json +11 -151
  81. package/lib/spec/context.json +2 -12
  82. package/lib/{ReproducingSystem.d.ts → systems/ReproducingSystem.d.ts} +3 -3
  83. package/lib/{TrackerBar.d.ts → systems/TrackerBar.d.ts} +48 -8
  84. package/lib/{TrackerBar.js → systems/TrackerBar.js} +11 -2
  85. package/lib/systems/index.d.ts +8 -2
  86. package/lib/systems/index.js +7 -1
  87. package/lib/systems/welte.d.ts +100 -0
  88. package/lib/systems/welte.js +119 -0
  89. package/lib/systems/welteLicensee/bar.d.ts +1 -1
  90. package/lib/systems/welteLicensee/bar.js +5 -2
  91. package/lib/systems/welteLicensee/system.d.ts +1 -1
  92. package/lib/systems/welteLicensee/system.js +1 -1
  93. package/lib/systems/welteT100/bar.d.ts +27 -1
  94. package/lib/systems/welteT100/bar.js +44 -2
  95. package/lib/systems/welteT100/system.d.ts +6 -39
  96. package/lib/systems/welteT100/system.js +19 -128
  97. package/lib/systems/welteT98/bar.d.ts +1 -1
  98. package/lib/systems/welteT98/bar.js +18 -2
  99. package/lib/systems/welteT98/system.d.ts +20 -38
  100. package/lib/systems/welteT98/system.js +20 -121
  101. package/lib/validate.d.ts +1 -1
  102. package/lib/{EditionView.d.ts → view/EditionView.d.ts} +38 -48
  103. package/lib/{EditionView.js → view/EditionView.js} +90 -96
  104. package/package.json +7 -2
  105. package/lib/Emulation.d.ts +0 -36
  106. package/lib/Emulation.js +0 -250
  107. package/lib/editionOps.d.ts +0 -260
  108. package/lib/editionOps.js +0 -1081
  109. package/lib/importJsonLd.js +0 -62
  110. /package/lib/{Agent.js → model/Agent.js} +0 -0
  111. /package/lib/{Assumption.js → model/Assumption.js} +0 -0
  112. /package/lib/{ConditionState.js → model/ConditionState.js} +0 -0
  113. /package/lib/{Edit.js → model/Edit.js} +0 -0
  114. /package/lib/{Edition.js → model/Edition.js} +0 -0
  115. /package/lib/{FeatureSource.js → model/FeatureSource.js} +0 -0
  116. /package/lib/{Perforator.js → model/Perforator.js} +0 -0
  117. /package/lib/{Quantity.d.ts → model/Quantity.d.ts} +0 -0
  118. /package/lib/{Quantity.js → model/Quantity.js} +0 -0
  119. /package/lib/{Symbol.js → model/Symbol.js} +0 -0
  120. /package/lib/{TrackCalibration.d.ts → model/TrackCalibration.d.ts} +0 -0
  121. /package/lib/{TrackCalibration.js → model/TrackCalibration.js} +0 -0
  122. /package/lib/{notes.d.ts → model/notes.d.ts} +0 -0
  123. /package/lib/{notes.js → model/notes.js} +0 -0
  124. /package/lib/{procedures.d.ts → model/procedures.d.ts} +0 -0
  125. /package/lib/{procedures.js → model/procedures.js} +0 -0
  126. /package/lib/{vocabulary.d.ts → model/vocabulary.d.ts} +0 -0
  127. /package/lib/{sorted.d.ts → shared/sorted.d.ts} +0 -0
  128. /package/lib/{sorted.js → shared/sorted.js} +0 -0
  129. /package/lib/{utils.d.ts → shared/utils.d.ts} +0 -0
  130. /package/lib/{utils.js → shared/utils.js} +0 -0
  131. /package/lib/{ReproducingSystem.js → systems/ReproducingSystem.js} +0 -0
@@ -0,0 +1,81 @@
1
+ import { EditionView } from "../view/EditionView.js";
2
+ /** What the operations are built from: the type of an operation, how one finds what it writes to on the draft, and the edits it writes. */
3
+ import { current, isDraft } from "immer";
4
+ import { v4 } from "uuid";
5
+ import { unchecked, uncheckedMotivation } from "../collation/Collation.js";
6
+ import { insertedBy } from "../model/Version.js";
7
+ import { featuresOf } from "../model/RollCopy.js";
8
+ import { assignReference } from "../model/Assumption.js";
9
+ import { without, pruned } from "./immutable.js";
10
+ export const noChange = () => undefined;
11
+ export const onCopy = (copyId, op) => draft => {
12
+ const copy = draft.copies.find(c => c.id === copyId);
13
+ if (copy)
14
+ op(copy, draft);
15
+ };
16
+ export const onVersion = (versionId, op) => draft => {
17
+ const version = draft.versions.find(v => v.id === versionId);
18
+ if (version)
19
+ op(version, draft);
20
+ };
21
+ export const onFeature = (copyId, featureId, op) => onCopy(copyId, copy => {
22
+ const feature = featuresOf(copy).find(f => f.id === featureId);
23
+ if (feature)
24
+ op(feature);
25
+ });
26
+ /**
27
+ * The operation built from a view of the state it is applied to. The
28
+ * view given is used where the draft still stands as the view saw it,
29
+ * and a fresh one is taken of the draft where an operation run before
30
+ * on the same draft has changed it: a path or an index read off the
31
+ * view would otherwise name what stood there before.
32
+ */
33
+ export const reading = (view, build) => draft => {
34
+ const state = stateOf(draft);
35
+ build(state === view.edition ? view : new EditionView(state))(draft);
36
+ };
37
+ /**
38
+ * The state a draft stands at, as plain data. Reading a draft proxies
39
+ * everything it touches, so what is only read is read from this.
40
+ */
41
+ export const stateOf = (draft) => isDraft(draft) ? current(draft) : draft;
42
+ /** The ids of the items the rewriting left out. */
43
+ export const droppedIds = (before, after) => {
44
+ const kept = new Set(after.map(item => item.id));
45
+ return before.flatMap(item => kept.has(item.id) ? [] : [item.id]);
46
+ };
47
+ export const insertion = (symbol) => ({ type: 'edit', id: v4(), insert: [symbol] });
48
+ export const deletion = (symbolId) => ({ type: 'edit', id: v4(), delete: [symbolId] });
49
+ /** The edit as a collation leaves it: made by rule, and not yet read by anybody. */
50
+ export const asUnchecked = (edit) => ({ ...edit, motivation: unchecked });
51
+ /**
52
+ * The motivations with the unchecked one declared, where any edit
53
+ * names it and the version does not state it yet. An edit naming a
54
+ * motivation the version leaves undeclared points at nothing.
55
+ */
56
+ export const declaring = (motivations, edits) => edits.some(edit => edit.motivation === unchecked)
57
+ && !motivations.some(motivation => motivation.id === unchecked)
58
+ ? [...motivations, uncheckedMotivation]
59
+ : motivations;
60
+ const isEmpty = (edit) => !edit.insert?.length && !edit.delete?.length;
61
+ export const insertedIn = (versions) => versions.flatMap(insertedBy);
62
+ /** The edits with the change applied, less those it emptied; the very same array where it changed none. */
63
+ export const edited = (edits, change) => pruned(edits, change, isEmpty);
64
+ /** The edit without the symbols among its insertions, or the very same edit where it inserts none of them. */
65
+ const droppingInsertions = (symbolIds) => (edit) => {
66
+ const insert = edit.insert && without(edit.insert, symbol => symbolIds.has(symbol.id));
67
+ return insert === edit.insert ? edit : { ...edit, insert };
68
+ };
69
+ /** Takes the symbols out of the version's own insertions, and the edits that had nothing else. */
70
+ export const dropInsertions = (version, symbolIds) => {
71
+ const edits = stateOf(version).edits;
72
+ if (edits)
73
+ version.edits = edited(edits, droppingInsertions(symbolIds));
74
+ };
75
+ /** A reference under the belief given, where one is given. */
76
+ export const referenceHeld = (id, belief) => ({ ...assignReference(id), ...(belief && { '@annotation': { id: v4(), belief } }) });
77
+ /** The references less those that match, or nothing where none is left. */
78
+ export const withoutReferences = (references, matches) => {
79
+ const kept = references.filter(reference => !matches(reference));
80
+ return kept.length > 0 ? kept : undefined;
81
+ };
@@ -0,0 +1,10 @@
1
+ import { EditionView } from "../view/EditionView.js";
2
+ import { Edit } from "../model/Edit.js";
3
+ import { EditionOp } from "./draft.js";
4
+ /**
5
+ * Replaces the edits with a single one carrying all their insertions
6
+ * and deletions, classified by a guess at what the exchange does.
7
+ */
8
+ export declare const mergeEdits: (given: EditionView, versionId: string, toMerge: readonly Edit[]) => EditionOp;
9
+ /** Replaces the edit with one edit per inserted and one per deleted symbol. */
10
+ export declare const splitEdit: (versionId: string, toSplit: Edit) => EditionOp;
@@ -0,0 +1,82 @@
1
+ /** Merging and splitting the edits of a version. */
2
+ import { v4 } from "uuid";
3
+ import { editsOf } from "../model/Version.js";
4
+ import { trackerBarOf } from "../systems/index.js";
5
+ import { distance, mm, subtract } from "../model/Quantity.js";
6
+ import { noChange, onVersion, insertion, deletion, reading } from "./draft.js";
7
+ const sameSequence = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
8
+ const expressionTypesOf = (symbols) => symbols.filter((symbol) => symbol.type === 'expression').map(symbol => symbol.expressionType);
9
+ /** The spellings of a single added accent the bar offers, none where no bar is known. */
10
+ const accentsOn = (bar) => bar?.accents ?? [];
11
+ const lengthOf = (span) => subtract(span.to, span.from);
12
+ /** How far apart two onsets may lie for the one symbol to count as a replacement of the other. */
13
+ const REPLACEMENT_TOLERANCE = mm(5);
14
+ /** Shorten or prolong, where the inserted symbol starts about where the deleted one did. */
15
+ const replacementType = (view, inserted, deleted) => {
16
+ const after = view.placeOf(inserted);
17
+ const before = view.placeOf(deleted);
18
+ if (!after || !before || distance(after.from, before.from) >= REPLACEMENT_TOLERANCE)
19
+ return undefined;
20
+ return lengthOf(after) < lengthOf(before) ? 'shorten' : 'prolong';
21
+ };
22
+ /**
23
+ * A guess at what an edit does, from the symbols it exchanges and from
24
+ * the systems the version and the one it is based on are coded for.
25
+ */
26
+ const guessEditType = (view, versionId, edit) => {
27
+ const inserts = edit.insert ?? [];
28
+ const deletes = view.symbols(edit.delete ?? []);
29
+ const inserted = expressionTypesOf(inserts);
30
+ const deleted = expressionTypesOf(deletes);
31
+ const bar = trackerBarOf(view.version(versionId)?.system);
32
+ const parentBar = trackerBarOf(view.predecessorOf(versionId)?.system);
33
+ /**
34
+ * Where the version is coded for another system than its parent, an
35
+ * exchange of expression matter is the transfer being carried out:
36
+ * a red ForzandoOn and ForzandoOff pair giving way to one held green
37
+ * SforzandoForte says the same thing in the other system's words,
38
+ * which is what 'replace-with-equivalent' is for. Calling it a
39
+ * corrected error would say the editor made a mistake.
40
+ */
41
+ if (bar && parentBar && bar.id !== parentBar.id && inserted.length > 0 && deleted.length > 0) {
42
+ return 'replace-with-equivalent';
43
+ }
44
+ if (deleted.length === 0 && accentsOn(bar).some(accent => sameSequence(inserted, accent)))
45
+ return 'additional-accent';
46
+ if (inserted.length > 1 && sameSequence(inserted, deleted))
47
+ return 'shift';
48
+ if (inserted.length === 0 && deleted.length === 1)
49
+ return 'remove-redundancy';
50
+ if (inserts.length === 1 && deletes.length === 1)
51
+ return replacementType(view, inserts[0], deletes[0]) ?? 'correct-error';
52
+ return 'correct-error';
53
+ };
54
+ /**
55
+ * Replaces the edits with a single one carrying all their insertions
56
+ * and deletions, classified by a guess at what the exchange does.
57
+ */
58
+ export const mergeEdits = (given, versionId, toMerge) => reading(given, view => {
59
+ if (toMerge.length === 0)
60
+ return noChange;
61
+ const merged = {
62
+ ...toMerge[0],
63
+ id: v4(),
64
+ insert: toMerge.flatMap(edit => edit.insert ?? []),
65
+ delete: toMerge.flatMap(edit => edit.delete ?? [])
66
+ };
67
+ merged.editType = guessEditType(view, versionId, merged);
68
+ const mergedIds = new Set(toMerge.map(edit => edit.id));
69
+ return onVersion(versionId, version => {
70
+ version.edits = [...editsOf(version).filter(edit => !mergedIds.has(edit.id)), merged];
71
+ });
72
+ });
73
+ /** Replaces the edit with one edit per inserted and one per deleted symbol. */
74
+ export const splitEdit = (versionId, toSplit) => {
75
+ const parts = [
76
+ ...(toSplit.insert ?? []).map(insertion),
77
+ ...(toSplit.delete ?? []).map(deletion)
78
+ ];
79
+ return onVersion(versionId, version => {
80
+ version.edits = [...editsOf(version).filter(edit => edit.id !== toSplit.id), ...parts];
81
+ });
82
+ };
@@ -0,0 +1,84 @@
1
+ import { EditionView } from "../view/EditionView.js";
2
+ import { ModificationPurpose } from "../model/RollCopy.js";
3
+ import { FeatureConditionAssignment, FeatureOrPatch, NestedFeature } from "../model/Feature.js";
4
+ import { EditionOp } from "./draft.js";
5
+ /**
6
+ * States the condition of the feature, in place of any earlier
7
+ * statement. Throws where the kind of feature is in no such condition;
8
+ * `conditions` says which conditions each kind of feature may be in.
9
+ */
10
+ export declare const stateFeatureCondition: (copyId: string, featureId: string, condition: FeatureConditionAssignment) => EditionOp;
11
+ /**
12
+ * The act a feature an editor states belongs to: the punching of the
13
+ * copy, the act that brought another feature about, or, where neither
14
+ * is said, an act of its own for the purpose given. A patch is never
15
+ * punched, being glued on, so `punched` says nothing of one.
16
+ */
17
+ export interface FeatureAct {
18
+ /** The copy came from its punching with it, which is where a reading of a scan puts every chain of holes. */
19
+ punched?: boolean;
20
+ /** The act that brought this feature about brought the new one about as well. */
21
+ beside?: string;
22
+ /** What the act was for, where a new one is made for the feature. */
23
+ purpose?: ModificationPurpose;
24
+ }
25
+ /**
26
+ * States that the copy bears the feature, in the act that brought it
27
+ * about: a patch was glued on by an attachment, anything else was
28
+ * produced, by the punching or by a later act. Which act, `FeatureAct`
29
+ * says; a `beside` that names nothing the copy bears is passed over and
30
+ * the feature goes into an act of its own.
31
+ */
32
+ export declare const addFeature: (copyId: string, feature: FeatureOrPatch, act?: FeatureAct) => EditionOp;
33
+ /**
34
+ * States that the patch bears the feature. What a patch bears came onto
35
+ * the copy with the patch, so it belongs to no act of its own and
36
+ * states no place: it stands where the patch stands.
37
+ */
38
+ export declare const addBorneFeature: (copyId: string, patchId: string, feature: NestedFeature) => EditionOp;
39
+ /**
40
+ * Takes the features off the copy, and out of the versions with what
41
+ * only they carried. A feature may be named wherever the copy bears it:
42
+ * a patch goes with everything glued onto it, and a feature of a patch
43
+ * may be taken back on its own, the patch staying where it is. An act
44
+ * left having produced or added nothing goes as well.
45
+ */
46
+ export declare const removeFeatures: (copyId: string, featureIds: readonly string[]) => EditionOp;
47
+ /** Why several features cannot be replaced by one. */
48
+ export type MergeObstacle = 'fewer-than-two' | 'different-types' | 'different-tracks' | 'differing-conditions' | 'unlike-features' | 'different-acts';
49
+ /**
50
+ * What stands in the way of reading the features as one, or nothing
51
+ * where they may be merged. Features may be merged where they differ
52
+ * in nothing but their place along the roll, their depiction and the
53
+ * condition at most one of them states. How far apart they lie is not
54
+ * asked: whether a gap is a bridge of the perforator, a tear or two
55
+ * perforations of their own is the editor's reading.
56
+ */
57
+ export declare const mergeObstacle: (features: readonly FeatureOrPatch[]) => MergeObstacle | undefined;
58
+ /**
59
+ * What stands in the way of reading the features the ids name as one,
60
+ * as `mergeFeatures` will find it. `mergeObstacle` asks only what the
61
+ * features themselves say, which cannot reach `different-acts`: one
62
+ * feature is the work of one act, and where the features stand is the
63
+ * edition's business rather than theirs.
64
+ */
65
+ export declare const mergeObstacleIn: (view: EditionView, featureIds: readonly string[]) => MergeObstacle | undefined;
66
+ /**
67
+ * Replaces the features of the copy with a single one covering them
68
+ * all, where a scan has split what the editor reads as one feature.
69
+ * The merged feature takes the place of the first it replaces and
70
+ * spans from the first to the last, any gap between them included.
71
+ * What the replaced features carried is carried by the merged one, and
72
+ * whatever else named them, a modification or a comprehension, names
73
+ * the merged one in their stead.
74
+ *
75
+ * Only the features the copy bears itself are merged. A feature of a
76
+ * patch states no place along the roll of its own, so there is nothing
77
+ * for a merge to span, and an id naming one is passed over as an id the
78
+ * copy does not bear is. Features of two different acts are not merged
79
+ * either: one feature is the work of one act.
80
+ *
81
+ * Throws where the features cannot stand for one; `mergeObstacle`
82
+ * says beforehand whether they can.
83
+ */
84
+ export declare const mergeFeatures: (copyId: string, featureIds: readonly string[]) => EditionOp;
@@ -0,0 +1,296 @@
1
+ import { v4 } from "uuid";
2
+ import { featuresByAct, featuresOf, statesNothing } from "../model/RollCopy.js";
3
+ import { idOf } from "../model/Assumption.js";
4
+ import { conditions as conditionsAllowed, featuresBorneBy, isPatch, withBorneFeatures } from "../model/Feature.js";
5
+ import { mm } from "../model/Quantity.js";
6
+ import { onCopy, onFeature, stateOf } from "./draft.js";
7
+ import { without, mapped, pruned, replacing, isRecord } from "./immutable.js";
8
+ import { renameReferences, forgetFeatures } from "./forget.js";
9
+ /**
10
+ * Whether the feature's own kind allows a condition of this kind. The
11
+ * type narrowed to is the weaker statement, holding of a condition any
12
+ * kind of feature may be in.
13
+ */
14
+ const allows = (feature, condition) => {
15
+ const allowed = conditionsAllowed[feature.type];
16
+ return allowed.includes(condition.conditionType);
17
+ };
18
+ /**
19
+ * States the condition of the feature, in place of any earlier
20
+ * statement. Throws where the kind of feature is in no such condition;
21
+ * `conditions` says which conditions each kind of feature may be in.
22
+ */
23
+ export const stateFeatureCondition = (copyId, featureId, condition) => onFeature(copyId, featureId, feature => {
24
+ if (!allows(feature, condition)) {
25
+ throw new Error(`A ${feature.type} is in no '${condition.conditionType}' condition`);
26
+ }
27
+ feature.condition = condition;
28
+ });
29
+ /**
30
+ * The ids of the features named and of everything they bear: a feature
31
+ * of a patch stands nowhere once the patch is gone.
32
+ */
33
+ const goneWith = (features, named) => features.flatMap(feature => named.has(feature.id)
34
+ ? withBorneFeatures(feature).map(borne => borne.id)
35
+ : goneWith(featuresBorneBy(feature), named));
36
+ /** The feature with the named ones gone from what it bears, or the very same one where it bears none of them. */
37
+ const withoutBorne = (feature, named) => {
38
+ if (!isPatch(feature) || !feature.features)
39
+ return feature;
40
+ const borne = withoutFeatures(feature.features, named);
41
+ return borne === feature.features ? feature : { ...feature, features: borne };
42
+ };
43
+ /** The features without those named, wherever they lie, and without whatever those bore. */
44
+ const withoutFeatures = (features, named) => mapped(without(features, feature => named.has(feature.id)), feature => withoutBorne(feature, named));
45
+ /** The act with its features rewritten, or the very same one where the rewriting left them. */
46
+ const rewritingAct = (rewrite) => (modification) => {
47
+ if (modification.type === 'Alteration')
48
+ return replacing(modification, 'produced', rewrite(modification.produced));
49
+ if (modification.type === 'Attachment')
50
+ return replacing(modification, 'added', rewrite(modification.added));
51
+ return modification;
52
+ };
53
+ /**
54
+ * Writes the rewriting onto the features of every act of the copy, and
55
+ * takes out an act it left with nothing to state.
56
+ */
57
+ const rewriteFeatures = (copy, rewrite) => {
58
+ const state = stateOf(copy);
59
+ const produced = state.production?.produced;
60
+ if (produced && copy.production) {
61
+ const rewritten = rewrite(produced);
62
+ if (rewritten !== produced)
63
+ copy.production.produced = rewritten;
64
+ }
65
+ copy.modifications = pruned(state.modifications, rewritingAct(rewrite), statesNothing);
66
+ };
67
+ /** The features of the act that brought the named feature about, where that act produces features. */
68
+ const producedBeside = (copy, featureId) => {
69
+ const names = (features) => features.some(feature => feature.id === featureId);
70
+ const punched = copy.production?.produced;
71
+ if (punched && names(punched))
72
+ return punched;
73
+ const act = copy.modifications.find(act => act.type === 'Alteration' && names(act.produced));
74
+ return act?.type === 'Alteration' ? act.produced : undefined;
75
+ };
76
+ /** The patches of the attachment that glued the named one on. */
77
+ const gluedBeside = (copy, patchId) => {
78
+ const act = copy.modifications.find(act => act.type === 'Attachment' && act.added.some(patch => patch.id === patchId));
79
+ return act?.type === 'Attachment' ? act.added : undefined;
80
+ };
81
+ /** The list the copy states a new feature in, making the act it belongs to where there is none. */
82
+ const actFor = (copy, patch, act) => {
83
+ const beside = act.beside !== undefined
84
+ ? (patch ? gluedBeside(copy, act.beside) : producedBeside(copy, act.beside))
85
+ : undefined;
86
+ if (beside)
87
+ return beside;
88
+ if (patch) {
89
+ const added = [];
90
+ copy.modifications.push({ type: 'Attachment', added, ...(act.purpose && { purpose: act.purpose }) });
91
+ return added;
92
+ }
93
+ if (act.punched) {
94
+ if (!copy.production)
95
+ copy.production = {};
96
+ if (!copy.production.produced)
97
+ copy.production.produced = [];
98
+ return copy.production.produced;
99
+ }
100
+ const produced = [];
101
+ copy.modifications.push({ type: 'Alteration', produced, ...(act.purpose && { purpose: act.purpose }) });
102
+ return produced;
103
+ };
104
+ /**
105
+ * States that the copy bears the feature, in the act that brought it
106
+ * about: a patch was glued on by an attachment, anything else was
107
+ * produced, by the punching or by a later act. Which act, `FeatureAct`
108
+ * says; a `beside` that names nothing the copy bears is passed over and
109
+ * the feature goes into an act of its own.
110
+ */
111
+ export const addFeature = (copyId, feature, act = {}) => onCopy(copyId, copy => {
112
+ actFor(copy, isPatch(feature), act).push(feature);
113
+ });
114
+ /**
115
+ * States that the patch bears the feature. What a patch bears came onto
116
+ * the copy with the patch, so it belongs to no act of its own and
117
+ * states no place: it stands where the patch stands.
118
+ */
119
+ export const addBorneFeature = (copyId, patchId, feature) => onCopy(copyId, copy => {
120
+ const patch = featuresOf(copy).flatMap(withBorneFeatures).find(borne => borne.id === patchId);
121
+ if (!patch || !isPatch(patch))
122
+ return;
123
+ if (!patch.features)
124
+ patch.features = [];
125
+ patch.features.push(feature);
126
+ });
127
+ /**
128
+ * Takes the features off the copy, and out of the versions with what
129
+ * only they carried. A feature may be named wherever the copy bears it:
130
+ * a patch goes with everything glued onto it, and a feature of a patch
131
+ * may be taken back on its own, the patch staying where it is. An act
132
+ * left having produced or added nothing goes as well.
133
+ */
134
+ export const removeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
135
+ const named = new Set(featureIds);
136
+ const features = featuresOf(stateOf(copy));
137
+ rewriteFeatures(copy, (stated) => withoutFeatures(stated, named));
138
+ forgetFeatures(draft, new Set(goneWith(features, named)));
139
+ });
140
+ /**
141
+ * Whether two records of the edition state the same, the identity of a
142
+ * statement left out: two transcriptions reading the same word say the
143
+ * same, whichever ids they were given.
144
+ */
145
+ const sayTheSame = (a, b) => {
146
+ if (a === b)
147
+ return true;
148
+ if (!isRecord(a) || !isRecord(b) || Array.isArray(a) !== Array.isArray(b))
149
+ return false;
150
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
151
+ keys.delete('id');
152
+ return [...keys].every(key => sayTheSame(a[key], b[key]));
153
+ };
154
+ /**
155
+ * What a feature states beyond its identity, its place along the roll,
156
+ * its depiction and its condition. What a patch bears counts by
157
+ * identity: two patches bear the same only where they bear the very
158
+ * same features, so that no merge takes a feature of a patch away.
159
+ */
160
+ const nature = (feature) => {
161
+ const { id, horizontal, depiction, condition, ...rest } = feature;
162
+ return isPatch(feature) && feature.features
163
+ ? { ...rest, features: feature.features.map(borne => borne.id) }
164
+ : rest;
165
+ };
166
+ const conditionsOf = (features) => features.flatMap(feature => feature.condition ? [feature.condition] : []);
167
+ /**
168
+ * What stands in the way of reading the features as one, or nothing
169
+ * where they may be merged. Features may be merged where they differ
170
+ * in nothing but their place along the roll, their depiction and the
171
+ * condition at most one of them states. How far apart they lie is not
172
+ * asked: whether a gap is a bridge of the perforator, a tear or two
173
+ * perforations of their own is the editor's reading.
174
+ */
175
+ export const mergeObstacle = (features) => {
176
+ if (features.length < 2)
177
+ return 'fewer-than-two';
178
+ const [first, ...rest] = features;
179
+ if (rest.some(feature => feature.type !== first.type))
180
+ return 'different-types';
181
+ if (rest.some(feature => !sayTheSame(feature.vertical, first.vertical)))
182
+ return 'different-tracks';
183
+ if (rest.some(feature => !sayTheSame(nature(feature), nature(first))))
184
+ return 'unlike-features';
185
+ const conditions = conditionsOf(features);
186
+ if (conditions.some(condition => !sayTheSame(condition, conditions[0])))
187
+ return 'differing-conditions';
188
+ return undefined;
189
+ };
190
+ /** The acts of the copy that state any of the named features. */
191
+ const actsHolding = (copy, named) => featuresByAct(copy).filter(features => features.some(feature => named.has(feature.id)));
192
+ /**
193
+ * What stands in the way of reading the named features of the copy as
194
+ * one, which is the whole of what `mergeFeatures` asks: whether one act
195
+ * brought them all about, and what the features themselves say. An id
196
+ * the copy does not bear at a place of its own is passed over.
197
+ */
198
+ const obstacleIn = (copy, featureIds) => {
199
+ const named = new Set(featureIds);
200
+ const acts = actsHolding(copy, named);
201
+ return acts.length > 1
202
+ ? 'different-acts'
203
+ : mergeObstacle((acts[0] ?? []).filter(feature => named.has(feature.id)));
204
+ };
205
+ /**
206
+ * What stands in the way of reading the features the ids name as one,
207
+ * as `mergeFeatures` will find it. `mergeObstacle` asks only what the
208
+ * features themselves say, which cannot reach `different-acts`: one
209
+ * feature is the work of one act, and where the features stand is the
210
+ * edition's business rather than theirs.
211
+ */
212
+ export const mergeObstacleIn = (view, featureIds) => {
213
+ const copy = featureIds.map(id => view.copyOf(id)).find(copy => copy !== undefined);
214
+ return copy ? obstacleIn(copy, featureIds) : 'fewer-than-two';
215
+ };
216
+ /** The items with the replacement in the place of the first it stands for, the others dropped. */
217
+ const standingFor = (items, stands, replacement) => {
218
+ const first = items.findIndex(stands);
219
+ if (first < 0)
220
+ return items;
221
+ return items.flatMap((item, i) => i === first ? [replacement] : stands(item) ? [] : [item]);
222
+ };
223
+ const spanning = (features) => ({
224
+ unit: 'mm',
225
+ from: mm(Math.min(...features.map(feature => feature.horizontal.from))),
226
+ to: mm(Math.max(...features.map(feature => feature.horizontal.to)))
227
+ });
228
+ /**
229
+ * The feature the merged ones give way to. It spans them all and is in
230
+ * every other respect the one of them that states a condition, the
231
+ * others being alike in all but their place. The depiction goes: a
232
+ * region of the scan showing one part depicts no more than that part.
233
+ */
234
+ const mergedFrom = (features, id) => {
235
+ const stating = features.find(feature => feature.condition) ?? features[0];
236
+ const merged = { ...stating, id, horizontal: spanning(features) };
237
+ delete merged.depiction;
238
+ return merged;
239
+ };
240
+ /** The symbol carried by the merged feature where it named one of those it replaces, and naming it once. */
241
+ const carriedByMerged = (replaced, mergedId) => (symbol) => {
242
+ const names = (carrier) => replaced.has(idOf(carrier));
243
+ const first = symbol.carriers.find(names);
244
+ if (!first)
245
+ return symbol;
246
+ return { ...symbol, carriers: standingFor(symbol.carriers, names, { ...first, id: mergedId }) };
247
+ };
248
+ /** Points the versions at the merged feature wherever they name one it replaces. */
249
+ const carryOver = (draft, replaced, mergedId) => {
250
+ const recarry = carriedByMerged(replaced, mergedId);
251
+ const recarrying = (edit) => {
252
+ const insert = edit.insert && mapped(edit.insert, recarry);
253
+ return insert === edit.insert ? edit : { ...edit, insert };
254
+ };
255
+ const versions = stateOf(draft.versions);
256
+ draft.versions.forEach((version, i) => {
257
+ const edits = versions[i].edits;
258
+ if (edits)
259
+ version.edits = mapped(edits, recarrying);
260
+ });
261
+ };
262
+ /**
263
+ * Replaces the features of the copy with a single one covering them
264
+ * all, where a scan has split what the editor reads as one feature.
265
+ * The merged feature takes the place of the first it replaces and
266
+ * spans from the first to the last, any gap between them included.
267
+ * What the replaced features carried is carried by the merged one, and
268
+ * whatever else named them, a modification or a comprehension, names
269
+ * the merged one in their stead.
270
+ *
271
+ * Only the features the copy bears itself are merged. A feature of a
272
+ * patch states no place along the roll of its own, so there is nothing
273
+ * for a merge to span, and an id naming one is passed over as an id the
274
+ * copy does not bear is. Features of two different acts are not merged
275
+ * either: one feature is the work of one act.
276
+ *
277
+ * Throws where the features cannot stand for one; `mergeObstacle`
278
+ * says beforehand whether they can.
279
+ */
280
+ export const mergeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
281
+ const state = stateOf(copy);
282
+ const obstacle = obstacleIn(state, featureIds);
283
+ if (obstacle) {
284
+ throw new Error(`The features of copy ${copyId} cannot be merged: ${obstacle}`);
285
+ }
286
+ const named = new Set(featureIds);
287
+ const toMerge = (actsHolding(state, named)[0] ?? []).filter(feature => named.has(feature.id));
288
+ const replaced = new Set(toMerge.map(feature => feature.id));
289
+ const mergedId = v4();
290
+ rewriteFeatures(copy, (stated) => {
291
+ const stands = (feature) => replaced.has(feature.id);
292
+ return stated.some(stands) ? standingFor(stated, stands, mergedFrom(stated.filter(stands), mergedId)) : stated;
293
+ });
294
+ carryOver(draft, replaced, mergedId);
295
+ renameReferences(draft, ids => standingFor(ids, id => replaced.has(id), mergedId));
296
+ });
@@ -0,0 +1,31 @@
1
+ /** Striking features from the edition, and rewriting the ids by which the edition names them outside the versions. */
2
+ import { Draft } from "immer";
3
+ import { Edition } from "../model/Edition.js";
4
+ import { AnySymbol } from "../model/Symbol.js";
5
+ import { RollCopy } from "../model/RollCopy.js";
6
+ import { Ids } from "./draft.js";
7
+ /** The ids of every feature the copy bears, those a patch bears among them. */
8
+ export declare const featureIdsOf: (copy: RollCopy) => Ids;
9
+ /**
10
+ * A symbol every carrier of which lies among the features loses its
11
+ * evidence with them. A symbol without carriers, such as a label,
12
+ * stands on its own.
13
+ */
14
+ export declare const carriedOnlyOn: (features: Ids) => (symbol: AnySymbol) => boolean;
15
+ /** The ids a statement names, as the rewriting leaves them. */
16
+ type Rename = (ids: string[]) => string[];
17
+ /**
18
+ * Rewrites the ids by which the edition names features and symbols
19
+ * outside the versions. A belief is annotatable anywhere, so the whole
20
+ * edition is read; what the rewriting leaves alone is left the very
21
+ * object it was.
22
+ */
23
+ export declare const renameReferences: (draft: Draft<Edition>, rename: Rename) => void;
24
+ /**
25
+ * Strikes the features from the edition: their carriers go, a symbol
26
+ * that had no other carrier goes with them, an edit left exchanging
27
+ * nothing goes as well, and so does every reference the versions, the
28
+ * modifications of the copies and the comprehensions made to what went.
29
+ */
30
+ export declare const forgetFeatures: (draft: Draft<Edition>, features: Ids) => void;
31
+ export {};