linked-rolls 0.14.0 → 0.15.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.
@@ -7,6 +7,7 @@ import { Edit } from "./Edit";
7
7
  import { RollCopy, ScaleReading, Shift } from "./RollCopy";
8
8
  import { FeatureSource } from "./FeatureSource";
9
9
  import { AnyArgumentation, Certainty } from "./Assumption";
10
+ import { AnyFeature } from "./Feature";
10
11
  /**
11
12
  * A change to an edition, written onto an immer draft of it. One
12
13
  * operation is one undo step, so an operation that has to read the
@@ -44,6 +45,28 @@ export declare const removeFeatures: (copyId: string, featureIds: readonly strin
44
45
  * carries, and with every reference the versions made to those symbols.
45
46
  */
46
47
  export declare const removeCopy: (copyId: string) => EditionOp;
48
+ /** Why several features cannot be replaced by one. */
49
+ export type MergeObstacle = 'fewer-than-two' | 'different-types' | 'different-tracks' | 'differing-conditions' | 'unlike-features';
50
+ /**
51
+ * What stands in the way of reading the features as one, or nothing
52
+ * where they may be merged. Features may be merged where they differ
53
+ * in nothing but their place along the roll, their depiction and the
54
+ * condition at most one of them states. How far apart they lie is not
55
+ * asked: whether a gap is a bridge of the perforator, a tear or two
56
+ * perforations of their own is the editor's reading.
57
+ */
58
+ export declare const mergeObstacle: (features: readonly AnyFeature[]) => MergeObstacle | undefined;
59
+ /**
60
+ * Replaces the features of the copy with a single one covering them
61
+ * all, where a scan has split what the editor reads as one feature.
62
+ * The merged feature takes the place of the first it replaces and
63
+ * spans from the first to the last, any gap between them included.
64
+ * What the replaced features carried is carried by the merged one.
65
+ *
66
+ * Throws where the features cannot stand for one; `mergeObstacle`
67
+ * says beforehand whether they can.
68
+ */
69
+ export declare const mergeFeatures: (copyId: string, featureIds: readonly string[]) => EditionOp;
47
70
  /**
48
71
  * Bases the child on the parent. A symbol of the child that collates
49
72
  * with one the parent hands down adds its carriers to that symbol; the
package/lib/editionOps.js CHANGED
@@ -173,6 +173,117 @@ export const removeCopy = (copyId) => onCopy(copyId, (copy, draft) => {
173
173
  forgetFeatures(draft, featureIdsOf(copy));
174
174
  draft.copies = draft.copies.filter(c => c.id !== copyId);
175
175
  });
176
+ const isRecord = (value) => typeof value === 'object' && value !== null;
177
+ /**
178
+ * Whether two records of the edition state the same, the identity of a
179
+ * statement left out: two transcriptions reading the same word say the
180
+ * same, whichever ids they were given.
181
+ */
182
+ const sayTheSame = (a, b) => {
183
+ if (a === b)
184
+ return true;
185
+ if (!isRecord(a) || !isRecord(b) || Array.isArray(a) !== Array.isArray(b))
186
+ return false;
187
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
188
+ keys.delete('id');
189
+ return [...keys].every(key => sayTheSame(a[key], b[key]));
190
+ };
191
+ /** What a feature states beyond its identity, its place along the roll, its depiction and its condition. */
192
+ const nature = (feature) => {
193
+ const { id, horizontal, depiction, condition, ...rest } = feature;
194
+ return rest;
195
+ };
196
+ const conditionsOf = (features) => features.flatMap(feature => feature.condition ? [feature.condition] : []);
197
+ /**
198
+ * What stands in the way of reading the features as one, or nothing
199
+ * where they may be merged. Features may be merged where they differ
200
+ * in nothing but their place along the roll, their depiction and the
201
+ * condition at most one of them states. How far apart they lie is not
202
+ * asked: whether a gap is a bridge of the perforator, a tear or two
203
+ * perforations of their own is the editor's reading.
204
+ */
205
+ export const mergeObstacle = (features) => {
206
+ if (features.length < 2)
207
+ return 'fewer-than-two';
208
+ const [first, ...rest] = features;
209
+ if (rest.some(feature => feature.type !== first.type))
210
+ return 'different-types';
211
+ if (rest.some(feature => !sayTheSame(feature.vertical, first.vertical)))
212
+ return 'different-tracks';
213
+ if (rest.some(feature => !sayTheSame(nature(feature), nature(first))))
214
+ return 'unlike-features';
215
+ const conditions = conditionsOf(features);
216
+ if (conditions.some(condition => !sayTheSame(condition, conditions[0])))
217
+ return 'differing-conditions';
218
+ return undefined;
219
+ };
220
+ /** The items with the replacement in the place of the first it stands for, the others dropped. */
221
+ const standingFor = (items, stands, replacement) => {
222
+ const first = items.findIndex(stands);
223
+ if (first < 0)
224
+ return items;
225
+ return items.flatMap((item, i) => i === first ? [replacement] : stands(item) ? [] : [item]);
226
+ };
227
+ const spanning = (features) => ({
228
+ unit: 'mm',
229
+ from: mm(Math.min(...features.map(feature => feature.horizontal.from))),
230
+ to: mm(Math.max(...features.map(feature => feature.horizontal.to)))
231
+ });
232
+ /**
233
+ * The feature the merged ones give way to. It spans them all and is in
234
+ * every other respect the one of them that states a condition, the
235
+ * others being alike in all but their place. The depiction goes: a
236
+ * region of the scan showing one part depicts no more than that part.
237
+ */
238
+ const mergedFrom = (features) => {
239
+ const stating = features.find(feature => feature.condition) ?? features[0];
240
+ const merged = { ...stating, id: v4(), horizontal: spanning(features) };
241
+ delete merged.depiction;
242
+ return merged;
243
+ };
244
+ /** The symbol carried by the merged feature where it named one of those it replaces, and naming it once. */
245
+ const carriedByMerged = (replaced, mergedId) => (symbol) => {
246
+ const names = (carrier) => replaced.has(idOf(carrier));
247
+ const first = symbol.carriers.find(names);
248
+ if (!first)
249
+ return symbol;
250
+ return { ...symbol, carriers: standingFor(symbol.carriers, names, { ...first, id: mergedId }) };
251
+ };
252
+ /** Points the versions at the merged feature wherever they name one it replaces. */
253
+ const carryOver = (draft, replaced, mergedId) => {
254
+ const recarry = carriedByMerged(replaced, mergedId);
255
+ const recarrying = (edit) => {
256
+ const insert = edit.insert && mapped(edit.insert, recarry);
257
+ return insert === edit.insert ? edit : { ...edit, insert };
258
+ };
259
+ const versions = stateOf(draft.versions);
260
+ draft.versions.forEach((version, i) => {
261
+ version.edits = mapped(versions[i].edits, recarrying);
262
+ });
263
+ };
264
+ /**
265
+ * Replaces the features of the copy with a single one covering them
266
+ * all, where a scan has split what the editor reads as one feature.
267
+ * The merged feature takes the place of the first it replaces and
268
+ * spans from the first to the last, any gap between them included.
269
+ * What the replaced features carried is carried by the merged one.
270
+ *
271
+ * Throws where the features cannot stand for one; `mergeObstacle`
272
+ * says beforehand whether they can.
273
+ */
274
+ export const mergeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
275
+ const replaced = new Set(featureIds);
276
+ const isReplaced = (feature) => replaced.has(feature.id);
277
+ const features = stateOf(copy.features);
278
+ const toMerge = features.filter(isReplaced);
279
+ const obstacle = mergeObstacle(toMerge);
280
+ if (obstacle) {
281
+ throw new Error(`The features of copy ${copyId} cannot be merged: ${obstacle}`);
282
+ }
283
+ const merged = mergedFrom(toMerge);
284
+ copy.features = standingFor(features, isReplaced, merged);
285
+ carryOver(draft, replaced, merged.id);
286
+ });
176
287
  /** The carriers of each collated symbol pass to its counterpart. */
177
288
  const handOverCarriers = (view, draft, collations) => collations.forEach(({ symbol, counterpart }) => {
178
289
  const path = view.getPath(counterpart.id);
package/lib/index.d.ts CHANGED
@@ -27,5 +27,6 @@ export * from './constraints';
27
27
  export * from './context';
28
28
  export * from './asJsonLd';
29
29
  export * from './importJsonLd';
30
+ export * from './migrate';
30
31
  export { readFromStanfordAton, type StanfordAtonOptions } from './readers/stanfordAton';
31
32
  export { readFromSpencerBar, readSpencerAnn, paperSpeedOfSpencerAnn, SPENCER_ROWS_PER_INCH, type SpencerBarOptions } from './readers/spencerBar';
package/lib/index.js CHANGED
@@ -27,5 +27,6 @@ export * from './constraints';
27
27
  export * from './context';
28
28
  export * from './asJsonLd';
29
29
  export * from './importJsonLd';
30
+ export * from './migrate';
30
31
  export { readFromStanfordAton } from './readers/stanfordAton';
31
32
  export { readFromSpencerBar, readSpencerAnn, paperSpeedOfSpencerAnn, SPENCER_ROWS_PER_INCH } from './readers/spencerBar';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.14.0",
3
+ "version": "0.15.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": {