linked-rolls 0.47.0 → 0.49.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.
@@ -85,6 +85,27 @@ export type Collation = {
85
85
  };
86
86
  /** Each of the own symbols with every inherited symbol it collates with, both in the order given. */
87
87
  export declare const collationsOf: (own: readonly Readonly<AnySymbol>[], inherited: readonly Readonly<AnySymbol>[], locate: Locate, tolerance?: CollationTolerance) => Collation[];
88
+ /**
89
+ * The motivation a collation writes on what it makes, saying that
90
+ * nobody has read it yet.
91
+ *
92
+ * A collation produces its edits by a rule, and an edition that
93
+ * publishes them unmarked would present a machine's output as an
94
+ * editor's reading. Stating that they are unchecked is the honest
95
+ * middle: the readings are there to be seen and argued with, and a
96
+ * reader can tell which of them anybody has weighed.
97
+ *
98
+ * It does not make an edit the editor's. `isCollationsOwn` passes over
99
+ * this motivation exactly, so a collation rewrites what it marked
100
+ * before, and only what somebody has actually written stays.
101
+ */
102
+ export declare const unchecked = "unchecked";
103
+ /** The version's statement of what the unchecked motivation means, to stand in its `motivations`. */
104
+ export declare const uncheckedMotivation: {
105
+ readonly type: 'motivation';
106
+ readonly id: "unchecked";
107
+ readonly note: 'Made by collating the two texts and not yet read by an editor.';
108
+ };
88
109
  /**
89
110
  * Whether the edit is one a collation writes by itself: a bare
90
111
  * insertion or a bare deletion saying nothing further, or an
package/lib/Collation.js CHANGED
@@ -89,6 +89,27 @@ export const collationsOf = (own, inherited, locate, tolerance = defaultCollatio
89
89
  };
90
90
  /** The edit type a collation draws by itself, from the two systems' vocabularies rather than off the paper. */
91
91
  const drawnByCollation = 'replace-with-equivalent';
92
+ /**
93
+ * The motivation a collation writes on what it makes, saying that
94
+ * nobody has read it yet.
95
+ *
96
+ * A collation produces its edits by a rule, and an edition that
97
+ * publishes them unmarked would present a machine's output as an
98
+ * editor's reading. Stating that they are unchecked is the honest
99
+ * middle: the readings are there to be seen and argued with, and a
100
+ * reader can tell which of them anybody has weighed.
101
+ *
102
+ * It does not make an edit the editor's. `isCollationsOwn` passes over
103
+ * this motivation exactly, so a collation rewrites what it marked
104
+ * before, and only what somebody has actually written stays.
105
+ */
106
+ export const unchecked = 'unchecked';
107
+ /** The version's statement of what the unchecked motivation means, to stand in its `motivations`. */
108
+ export const uncheckedMotivation = {
109
+ type: 'motivation',
110
+ id: unchecked,
111
+ note: 'Made by collating the two texts and not yet read by an editor.'
112
+ };
92
113
  /**
93
114
  * Whether the edit is one a collation writes by itself: a bare
94
115
  * insertion or a bare deletion saying nothing further, or an
@@ -107,4 +128,6 @@ const drawnByCollation = 'replace-with-equivalent';
107
128
  * draws a second time over the very same symbols.
108
129
  */
109
130
  export const isCollationsOwn = (edit) => edit.editType === drawnByCollation
110
- || (edit.editType === undefined && edit.motivation === undefined && edit['@annotation'] === undefined);
131
+ || (edit.editType === undefined
132
+ && (edit.motivation === undefined || edit.motivation === unchecked)
133
+ && edit['@annotation'] === undefined);
package/lib/RollCopy.d.ts CHANGED
@@ -259,16 +259,28 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
259
259
  */
260
260
  scale: number;
261
261
  /**
262
- * How much longer this copy's reading of a hole ran than the
263
- * perforation that caused it, where that was taken off the ends
264
- * again. A pneumatic reader reports how long a valve stayed
265
- * open, which exceeds the perforation that opened it, so its
266
- * holes are overlong by a constant while its onsets agree.
262
+ * What a pneumatic reader added to this copy's holes, where it
263
+ * was taken off again. Such a reader reports how long a valve
264
+ * stayed open, which exceeds the perforation that opened it, so
265
+ * its holes are overlong by a constant while its onsets agree.
267
266
  * Nothing for a copy read by other means, whose holes are the
268
267
  * punched slots themselves.
269
268
  * Not exported to RDF.
270
269
  */
271
- readerExtension: Millimeters;
270
+ readerExtension: {
271
+ /** How much longer the reading of a hole ran than the perforation that caused it. */
272
+ length: Millimeters;
273
+ /**
274
+ * The holes it was not taken off, by `@id`, being no longer
275
+ * than it is. The constant reaches its limit there rather
276
+ * than the copy being wrong, and why they were left is for
277
+ * the edition to state about the features themselves; this
278
+ * only records that they stand as the reader gave them, so
279
+ * that putting the extension back does not lengthen a hole
280
+ * nothing was taken from.
281
+ */
282
+ leaving?: string[];
283
+ };
272
284
  /**
273
285
  * The resolution this copy's scan was read at, along the roll.
274
286
  * It is what the measurements given in pixels are to be read
@@ -32,8 +32,32 @@ export declare const revertScale: (copy: RollCopy) => void;
32
32
  *
33
33
  * Only holes are touched. What a writing or a mark spans is no valve.
34
34
  */
35
- export declare const shortenHoles: (extension: Millimeters, copy: RollCopy) => void;
36
- /** Puts the reader's extension back on the copy's holes, as far as one was taken off. */
35
+ /**
36
+ * The holes the extension cannot be taken off, being no longer than it
37
+ * is: the reader reported them open for less time than it holds a valve
38
+ * on beyond the perforation, so the constant over-corrects them and
39
+ * would leave them ending before they begin.
40
+ *
41
+ * They are where the constant shows its limit rather than where the
42
+ * copy is wrong, the extension varying by about half a millimetre with
43
+ * the port. What to do with them is an editorial question — a condition
44
+ * on the feature, a reading of the hole, a smaller extension — and
45
+ * `shortenHoles` throws rather than answer it.
46
+ */
47
+ export declare const tooShortToShorten: (extension: Millimeters, copy: RollCopy, leaving?: ReadonlySet<string>) => Readonly<FeatureOrPatch>[];
48
+ /**
49
+ * Takes the extension off, leaving the named holes as the reader gave
50
+ * them. Naming a hole is an editorial act and not a repair: it says
51
+ * this one is where the constant stops applying, and the edition is
52
+ * what has to say why. The copy records which were left, so that
53
+ * putting the extension back does not lengthen a hole nothing was
54
+ * taken from.
55
+ *
56
+ * Throws where a hole that was not named is no longer than the
57
+ * extension, `tooShortToShorten` saying beforehand which those are.
58
+ */
59
+ export declare const shortenHoles: (extension: Millimeters, copy: RollCopy, leaving?: ReadonlySet<string>) => void;
60
+ /** Puts the reader's extension back on the holes it was taken off, as far as one was taken off. */
37
61
  export declare const revertShortening: (copy: RollCopy) => void;
38
62
  /**
39
63
  * How a copy's places are carried onto another copy's:
package/lib/alignment.js CHANGED
@@ -78,19 +78,55 @@ const holesOf = (copy) => featuresOf(copy).filter(feature => feature.type === 'H
78
78
  *
79
79
  * Only holes are touched. What a writing or a mark spans is no valve.
80
80
  */
81
- export const shortenHoles = (extension, copy) => {
81
+ /**
82
+ * The holes the extension cannot be taken off, being no longer than it
83
+ * is: the reader reported them open for less time than it holds a valve
84
+ * on beyond the perforation, so the constant over-corrects them and
85
+ * would leave them ending before they begin.
86
+ *
87
+ * They are where the constant shows its limit rather than where the
88
+ * copy is wrong, the extension varying by about half a millimetre with
89
+ * the port. What to do with them is an editorial question — a condition
90
+ * on the feature, a reading of the hole, a smaller extension — and
91
+ * `shortenHoles` throws rather than answer it.
92
+ */
93
+ export const tooShortToShorten = (extension, copy, leaving = new Set()) => holesOf(copy).filter(hole => !leaving.has(hole.id) && hole.horizontal.to - hole.horizontal.from <= extension);
94
+ /**
95
+ * Takes the extension off, leaving the named holes as the reader gave
96
+ * them. Naming a hole is an editorial act and not a repair: it says
97
+ * this one is where the constant stops applying, and the edition is
98
+ * what has to say why. The copy records which were left, so that
99
+ * putting the extension back does not lengthen a hole nothing was
100
+ * taken from.
101
+ *
102
+ * Throws where a hole that was not named is no longer than the
103
+ * extension, `tooShortToShorten` saying beforehand which those are.
104
+ */
105
+ export const shortenHoles = (extension, copy, leaving = new Set()) => {
82
106
  if (copy.ops.includes('shortened'))
83
107
  return;
84
- holesOf(copy).forEach(hole => { hole.horizontal.to = subtract(hole.horizontal.to, extension); });
108
+ const tooShort = tooShortToShorten(extension, copy, leaving);
109
+ if (tooShort.length > 0) {
110
+ throw new Error(`The extension of ${extension} mm cannot be taken off ${tooShort.length} `
111
+ + `hole(s) of copy ${copy.id}, which are no longer than it: `
112
+ + `${tooShort.map(hole => hole.id).join(', ')}`);
113
+ }
114
+ const left = holesOf(copy).filter(hole => leaving.has(hole.id)).map(hole => hole.id);
115
+ holesOf(copy)
116
+ .filter(hole => !leaving.has(hole.id))
117
+ .forEach(hole => { hole.horizontal.to = subtract(hole.horizontal.to, extension); });
85
118
  copy.ops = [...copy.ops, 'shortened'];
86
- copy.measurements.readerExtension = extension;
119
+ copy.measurements.readerExtension = { length: extension, ...(left.length > 0 && { leaving: left }) };
87
120
  };
88
- /** Puts the reader's extension back on the copy's holes, as far as one was taken off. */
121
+ /** Puts the reader's extension back on the holes it was taken off, as far as one was taken off. */
89
122
  export const revertShortening = (copy) => {
90
- const extension = copy.measurements.readerExtension;
91
- if (!copy.ops.includes('shortened') || extension === undefined)
123
+ const taken = copy.measurements.readerExtension;
124
+ if (!copy.ops.includes('shortened') || taken === undefined)
92
125
  return;
93
- holesOf(copy).forEach(hole => { hole.horizontal.to = add(hole.horizontal.to, extension); });
126
+ const left = new Set(taken.leaving ?? []);
127
+ holesOf(copy)
128
+ .filter(hole => !left.has(hole.id))
129
+ .forEach(hole => { hole.horizontal.to = add(hole.horizontal.to, taken.length); });
94
130
  copy.ops = copy.ops.filter(op => op !== 'shortened');
95
131
  delete copy.measurements.readerExtension;
96
132
  };
@@ -46,7 +46,7 @@ export declare const unalignCopy: (copyId: string) => EditionOp;
46
46
  * copy's holes, and records how much was taken, so that its lengths
47
47
  * can be compared with a scanned copy's at all.
48
48
  */
49
- export declare const shortenCopy: (copyId: string, extension: Millimeters) => EditionOp;
49
+ export declare const shortenCopy: (copyId: string, extension: Millimeters, leaving?: ReadonlySet<string>) => EditionOp;
50
50
  /** Puts the reader's extension back on the copy's holes. */
51
51
  export declare const unshortenCopy: (copyId: string) => EditionOp;
52
52
  /** States what the copy's features were read from, in place of any earlier statement. */
package/lib/editionOps.js CHANGED
@@ -2,7 +2,7 @@ import { current, isDraft } from "immer";
2
2
  import { v4 } from "uuid";
3
3
  import { getAt } from "./EditionView.js";
4
4
  import { isCommand, placementRelations } from "./Symbol.js";
5
- import { collationsOf, defaultCollationTolerance, isCollationsOwn } from "./Collation.js";
5
+ import { collationsOf, defaultCollationTolerance, isCollationsOwn, unchecked, uncheckedMotivation } from "./Collation.js";
6
6
  import { collationToleranceOf, editsOf, insertedBy, principalDerivationOf } from "./Version.js";
7
7
  import { asSymbols, barOf, featuresByAct, featuresOf, isPaperStretch, statesNothing } from "./RollCopy.js";
8
8
  import { systemOf } from "./TrackerBar.js";
@@ -54,6 +54,17 @@ const droppedIds = (before, after) => {
54
54
  };
55
55
  const insertion = (symbol) => ({ type: 'edit', id: v4(), insert: [symbol] });
56
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;
57
68
  const isEmpty = (edit) => !edit.insert?.length && !edit.delete?.length;
58
69
  const insertedIn = (versions) => versions.flatMap(insertedBy);
59
70
  /** The edits with the change applied, less those it emptied; the very same array where it changed none. */
@@ -130,7 +141,7 @@ export const unalignCopy = (copyId) => onCopy(copyId, copy => {
130
141
  * copy's holes, and records how much was taken, so that its lengths
131
142
  * can be compared with a scanned copy's at all.
132
143
  */
133
- export const shortenCopy = (copyId, extension) => onCopy(copyId, copy => shortenHoles(extension, copy));
144
+ export const shortenCopy = (copyId, extension, leaving) => onCopy(copyId, copy => shortenHoles(extension, copy, leaving));
134
145
  /** Puts the reader's extension back on the copy's holes. */
135
146
  export const unshortenCopy = (copyId) => onCopy(copyId, copy => revertShortening(copy));
136
147
  /** States what the copy's features were read from, in place of any earlier statement. */
@@ -733,6 +744,7 @@ export const connectVersions = (view, childId, parentId, tolerance = defaultColl
733
744
  type: 'edit',
734
745
  id: v4(),
735
746
  editType: 'replace-with-equivalent',
747
+ motivation: unchecked,
736
748
  insert: [...by],
737
749
  delete: deleted
738
750
  };
@@ -740,14 +752,15 @@ export const connectVersions = (view, childId, parentId, tolerance = defaultColl
740
752
  const edits = [
741
753
  ...established,
742
754
  ...substituted.map(equivalence),
743
- ...own.filter(symbol => !collated.has(symbol.id) && !paired.has(symbol.id)).map(insertion),
755
+ ...own.filter(symbol => !collated.has(symbol.id) && !paired.has(symbol.id)).map(insertion).map(asUnchecked),
744
756
  ...inherited
745
757
  .filter(symbol => !matched.has(symbol.id) && !paired.has(symbol.id))
746
- .map(symbol => deletion(symbol.id))
758
+ .map(symbol => asUnchecked(deletion(symbol.id)))
747
759
  ];
748
760
  return onVersion(childId, (child, draft) => {
749
761
  handOverCarriers(view, draft, collations);
750
762
  child.edits = edits;
763
+ child.motivations = declaring(stateOf(child).motivations, edits);
751
764
  child.basedOn = [
752
765
  { ...assignReference(parentId), collationTolerance: tolerance },
753
766
  ...hypothesesBeside(stateOf(child), parentId)
package/lib/schema.json CHANGED
@@ -2829,8 +2829,24 @@
2829
2829
  "ontology": "reo:punchDiameter"
2830
2830
  },
2831
2831
  "readerExtension": {
2832
- "$ref": "#/definitions/Millimeters",
2833
- "description": "How much longer this copy's reading of a hole ran than the perforation that caused it, where that was taken off the ends again. A pneumatic reader reports how long a valve stayed open, which exceeds the perforation that opened it, so its holes are overlong by a constant while its onsets agree. Nothing for a copy read by other means, whose holes are the punched slots themselves. Not exported to RDF."
2832
+ "description": "What a pneumatic reader added to this copy's holes, where it was taken off again. Such a reader reports how long a valve stayed open, which exceeds the perforation that opened it, so its holes are overlong by a constant while its onsets agree. Nothing for a copy read by other means, whose holes are the punched slots themselves. Not exported to RDF.",
2833
+ "properties": {
2834
+ "leaving": {
2835
+ "description": "The holes it was not taken off, by `@id`, being no longer than it is. The constant reaches its limit there rather than the copy being wrong, and why they were left is for the edition to state about the features themselves; this only records that they stand as the reader gave them, so that putting the extension back does not lengthen a hole nothing was taken from.",
2836
+ "items": {
2837
+ "type": "string"
2838
+ },
2839
+ "type": "array"
2840
+ },
2841
+ "length": {
2842
+ "$ref": "#/definitions/Millimeters",
2843
+ "description": "How much longer the reading of a hole ran than the perforation that caused it."
2844
+ }
2845
+ },
2846
+ "required": [
2847
+ "length"
2848
+ ],
2849
+ "type": "object"
2834
2850
  },
2835
2851
  "scale": {
2836
2852
  "description": "The factor this copy's features were scaled by to align them with the others. What it is put down to is stated apart: a paper-stretch condition, or the speed the copy was cut for. Not exported to RDF.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.47.0",
3
+ "version": "0.49.0",
4
4
  "description": "Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system",
5
5
  "license": "MIT",
6
6
  "repository": {