« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data
diff options
context:
space:
mode:
Diffstat (limited to 'src/data')
-rw-r--r--src/data/composite/control-flow/flipFilter.js36
-rw-r--r--src/data/composite/control-flow/index.js1
-rw-r--r--src/data/composite/control-flow/withAvailabilityFilter.js1
-rw-r--r--src/data/composite/data/withFilteredList.js16
-rw-r--r--src/data/composite/data/withNearbyItemFromList.js52
-rw-r--r--src/data/composite/data/withPropertyFromObject.js38
-rw-r--r--src/data/composite/things/artwork/index.js4
-rw-r--r--src/data/composite/things/artwork/withAttachedArtwork.js43
-rw-r--r--src/data/composite/things/artwork/withContainingArtworkList.js46
-rw-r--r--src/data/composite/things/artwork/withContribsFromAttachedArtwork.js28
-rw-r--r--src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js65
-rw-r--r--src/data/composite/wiki-data/withConstitutedArtwork.js4
-rw-r--r--src/data/composite/wiki-properties/constitutibleArtwork.js2
-rw-r--r--src/data/composite/wiki-properties/constitutibleArtworkList.js2
-rw-r--r--src/data/things/album.js3
-rw-r--r--src/data/things/artist.js1
-rw-r--r--src/data/things/artwork.js87
-rw-r--r--src/data/things/flash.js1
-rw-r--r--src/data/things/track.js1
-rw-r--r--src/data/yaml.js2
20 files changed, 387 insertions, 46 deletions
diff --git a/src/data/composite/control-flow/flipFilter.js b/src/data/composite/control-flow/flipFilter.js
new file mode 100644
index 00000000..995bacad
--- /dev/null
+++ b/src/data/composite/control-flow/flipFilter.js
@@ -0,0 +1,36 @@
+// Flips a filter, so that each true item becomes false, and vice versa.
+// Overwrites the provided dependency.
+//
+// See also:
+//  - withAvailabilityFilter
+
+import {input, templateCompositeFrom} from '#composite';
+
+export default templateCompositeFrom({
+  annotation: `flipFilter`,
+
+  inputs: {
+    filter: input({type: 'array'}),
+  },
+
+  outputs: ({
+    [input.staticDependency('filter')]: filterDependency,
+  }) => [filterDependency ?? '#flippedFilter'],
+
+  steps: () => [
+    {
+      dependencies: [
+        input('filter'),
+        input.staticDependency('filter'),
+      ],
+
+      compute: (continuation, {
+        [input('filter')]: filter,
+        [input.staticDependency('filter')]: filterDependency,
+      }) => continuation({
+        [filterDependency ?? '#flippedFilter']:
+          filter.map(item => !item),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/control-flow/index.js b/src/data/composite/control-flow/index.js
index 7e137a14..778dc66b 100644
--- a/src/data/composite/control-flow/index.js
+++ b/src/data/composite/control-flow/index.js
@@ -10,6 +10,7 @@ export {default as exposeDependency} from './exposeDependency.js';
 export {default as exposeDependencyOrContinue} from './exposeDependencyOrContinue.js';
 export {default as exposeUpdateValueOrContinue} from './exposeUpdateValueOrContinue.js';
 export {default as exposeWhetherDependencyAvailable} from './exposeWhetherDependencyAvailable.js';
+export {default as flipFilter} from './flipFilter.js';
 export {default as raiseOutputWithoutDependency} from './raiseOutputWithoutDependency.js';
 export {default as raiseOutputWithoutUpdateValue} from './raiseOutputWithoutUpdateValue.js';
 export {default as withAvailabilityFilter} from './withAvailabilityFilter.js';
diff --git a/src/data/composite/control-flow/withAvailabilityFilter.js b/src/data/composite/control-flow/withAvailabilityFilter.js
index cfea998e..fd93af71 100644
--- a/src/data/composite/control-flow/withAvailabilityFilter.js
+++ b/src/data/composite/control-flow/withAvailabilityFilter.js
@@ -4,6 +4,7 @@
 // Accepts the same mode options as withResultOfAvailabilityCheck.
 //
 // See also:
+//  - flipFilter
 //  - withFilteredList
 //  - withResultOfAvailabilityCheck
 //
diff --git a/src/data/composite/data/withFilteredList.js b/src/data/composite/data/withFilteredList.js
index 44c1661d..15ee3373 100644
--- a/src/data/composite/data/withFilteredList.js
+++ b/src/data/composite/data/withFilteredList.js
@@ -2,9 +2,6 @@
 // corresponding items in a list. Items which correspond to a truthy value
 // are kept, and the rest are excluded from the output list.
 //
-// If the flip option is set, only items corresponding with a *falsy* value in
-// the filter are kept.
-//
 // TODO: There should be two outputs - one for the items included according to
 // the filter, and one for the items excluded.
 //
@@ -22,28 +19,19 @@ export default templateCompositeFrom({
   inputs: {
     list: input({type: 'array'}),
     filter: input({type: 'array'}),
-
-    flip: input({
-      type: 'boolean',
-      defaultValue: false,
-    }),
   },
 
   outputs: ['#filteredList'],
 
   steps: () => [
     {
-      dependencies: [input('list'), input('filter'), input('flip')],
+      dependencies: [input('list'), input('filter')],
       compute: (continuation, {
         [input('list')]: list,
         [input('filter')]: filter,
-        [input('flip')]: flip,
       }) => continuation({
         '#filteredList':
-          list.filter((_item, index) =>
-            (flip
-              ? !filter[index]
-              :  filter[index])),
+          list.filter((_item, index) => filter[index]),
       }),
     },
   ],
diff --git a/src/data/composite/data/withNearbyItemFromList.js b/src/data/composite/data/withNearbyItemFromList.js
index 83a8cc21..5e165219 100644
--- a/src/data/composite/data/withNearbyItemFromList.js
+++ b/src/data/composite/data/withNearbyItemFromList.js
@@ -9,6 +9,10 @@
 //  - If the 'valuePastEdge' input is provided, that value will be output
 //    instead of null.
 //
+//  - If the 'filter' input is provided, corresponding items will be skipped,
+//    and only (repeating `offset`) the next included in the filter will be
+//    returned.
+//
 // Both the list and item must be provided.
 //
 // See also:
@@ -16,7 +20,6 @@
 //
 
 import {input, templateCompositeFrom} from '#composite';
-import {atOffset} from '#sugar';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
 
@@ -28,9 +31,12 @@ export default templateCompositeFrom({
   inputs: {
     list: input({acceptsNull: false, type: 'array'}),
     item: input({acceptsNull: false}),
-
     offset: input({type: 'number'}),
+
     wrap: input({type: 'boolean', defaultValue: false}),
+    valuePastEdge: input({defaultValue: null}),
+
+    filter: input({defaultValue: null, type: 'array'}),
   },
 
   outputs: ['#nearbyItem'],
@@ -45,29 +51,55 @@ export default templateCompositeFrom({
       dependency: '#index',
       mode: input.value('index'),
 
-      output: input.value({
-        ['#nearbyItem']:
-          null,
-      }),
+      output: input.value({'#nearbyItem': null}),
     }),
 
     {
       dependencies: [
         input('list'),
         input('offset'),
+
         input('wrap'),
+        input('valuePastEdge'),
+
+        input('filter'),
+
         '#index',
       ],
 
       compute: (continuation, {
         [input('list')]: list,
         [input('offset')]: offset,
+
         [input('wrap')]: wrap,
+        [input('valuePastEdge')]: valuePastEdge,
+
+        [input('filter')]: filter,
+
         ['#index']: index,
-      }) => continuation({
-        ['#nearbyItem']:
-          atOffset(list, index, offset, {wrap}),
-      }),
+      }) => {
+        const startIndex = index;
+
+        do {
+          index += offset;
+
+          if (wrap) {
+            index = index % list.length;
+          } else if (index < 0) {
+            return continuation({'#nearbyItem': valuePastEdge});
+          } else if (index >= list.length) {
+            return continuation({'#nearbyItem': valuePastEdge});
+          }
+
+          if (filter && !filter[index]) {
+            continue;
+          }
+
+          return continuation({'#nearbyItem': list[index]});
+        } while (index !== startIndex);
+
+        return continuation({'#nearbyItem': null});
+      },
     },
   ],
 });
diff --git a/src/data/composite/data/withPropertyFromObject.js b/src/data/composite/data/withPropertyFromObject.js
index 4f240506..7b452b99 100644
--- a/src/data/composite/data/withPropertyFromObject.js
+++ b/src/data/composite/data/withPropertyFromObject.js
@@ -13,6 +13,21 @@
 import CacheableObject from '#cacheable-object';
 import {input, templateCompositeFrom} from '#composite';
 
+function getOutputName({
+  [input.staticDependency('object')]: object,
+  [input.staticValue('property')]: property,
+}) {
+  if (object && property) {
+    if (object.startsWith('#')) {
+      return `${object}.${property}`;
+    } else {
+      return `#${object}.${property}`;
+    }
+  } else {
+    return '#value';
+  }
+}
+
 export default templateCompositeFrom({
   annotation: `withPropertyFromObject`,
 
@@ -22,15 +37,7 @@ export default templateCompositeFrom({
     internal: input({type: 'boolean', defaultValue: false}),
   },
 
-  outputs: ({
-    [input.staticDependency('object')]: object,
-    [input.staticValue('property')]: property,
-  }) =>
-    (object && property
-      ? (object.startsWith('#')
-          ? [`${object}.${property}`]
-          : [`#${object}.${property}`])
-      : ['#value']),
+  outputs: inputs => [getOutputName(inputs)],
 
   steps: () => [
     {
@@ -39,17 +46,8 @@ export default templateCompositeFrom({
         input.staticValue('property'),
       ],
 
-      compute: (continuation, {
-        [input.staticDependency('object')]: object,
-        [input.staticValue('property')]: property,
-      }) => continuation({
-        '#output':
-          (object && property
-            ? (object.startsWith('#')
-                ? `${object}.${property}`
-                : `#${object}.${property}`)
-            : '#value'),
-      }),
+      compute: (continuation, inputs) =>
+        continuation({'#output': getOutputName(inputs)}),
     },
 
     {
diff --git a/src/data/composite/things/artwork/index.js b/src/data/composite/things/artwork/index.js
index b92bff72..3693c10f 100644
--- a/src/data/composite/things/artwork/index.js
+++ b/src/data/composite/things/artwork/index.js
@@ -1 +1,5 @@
+export {default as withAttachedArtwork} from './withAttachedArtwork.js';
+export {default as withContainingArtworkList} from './withContainingArtworkList.js';
+export {default as withContribsFromAttachedArtwork} from './withContribsFromAttachedArtwork.js';
 export {default as withDate} from './withDate.js';
+export {default as withPropertyFromAttachedArtwork} from './withPropertyFromAttachedArtwork.js';
diff --git a/src/data/composite/things/artwork/withAttachedArtwork.js b/src/data/composite/things/artwork/withAttachedArtwork.js
new file mode 100644
index 00000000..d7c0d87b
--- /dev/null
+++ b/src/data/composite/things/artwork/withAttachedArtwork.js
@@ -0,0 +1,43 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {flipFilter, raiseOutputWithoutDependency}
+  from '#composite/control-flow';
+import {withNearbyItemFromList, withPropertyFromList} from '#composite/data';
+
+import withContainingArtworkList from './withContainingArtworkList.js';
+
+export default templateCompositeFrom({
+  annotaion: `withContribsFromMainArtwork`,
+
+  outputs: ['#attachedArtwork'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: 'attachAbove',
+      mode: input.value('falsy'),
+      output: input.value({'#attachedArtwork': null}),
+    }),
+
+    withContainingArtworkList(),
+
+    withPropertyFromList({
+      list: '#containingArtworkList',
+      property: input.value('attachAbove'),
+    }),
+
+    flipFilter({
+      filter: '#containingArtworkList.attachAbove',
+    }).outputs({
+      '#containingArtworkList.attachAbove': '#filterNotAttached',
+    }),
+
+    withNearbyItemFromList({
+      list: '#containingArtworkList',
+      item: input.myself(),
+      offset: input.value(-1),
+      filter: '#filterNotAttached',
+    }).outputs({
+      '#nearbyItem': '#attachedArtwork',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/artwork/withContainingArtworkList.js b/src/data/composite/things/artwork/withContainingArtworkList.js
new file mode 100644
index 00000000..9c928ffd
--- /dev/null
+++ b/src/data/composite/things/artwork/withContainingArtworkList.js
@@ -0,0 +1,46 @@
+// Gets the list of artworks which contains this one, which is functionally
+// equivalent to `this.thing[this.thingProperty]`. If the exposed value is not
+// a list at all (i.e. the property holds a single artwork), this composition
+// outputs null.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+export default templateCompositeFrom({
+  annotation: `withContainingArtworkList`,
+
+  outputs: ['#containingArtworkList'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: 'thing',
+      output: input.value({'#containingArtworkList': null}),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: 'thingProperty',
+      output: input.value({'#containingArtworkList': null}),
+    }),
+
+    withPropertyFromObject({
+      object: 'thing',
+      property: 'thingProperty',
+    }).outputs({
+      '#value': '#containingValue',
+    }),
+
+    {
+      dependencies: ['#containingValue'],
+      compute: (continuation, {
+        ['#containingValue']: containingValue,
+      }) => continuation({
+        ['#containingArtworkList']:
+          (Array.isArray(containingValue)
+            ? containingValue
+            : null),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js
new file mode 100644
index 00000000..36abb3fe
--- /dev/null
+++ b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js
@@ -0,0 +1,28 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+import {withRecontextualizedContributionList} from '#composite/wiki-data';
+
+import withPropertyFromAttachedArtwork from './withPropertyFromAttachedArtwork.js';
+
+export default templateCompositeFrom({
+  annotaion: `withContribsFromAttachedArtwork`,
+
+  outputs: ['#attachedArtwork.artistContribs'],
+
+  steps: () => [
+    withPropertyFromAttachedArtwork({
+      property: input.value('artistContribs'),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#attachedArtwork.artistContribs',
+      output: input.value({'#attachedArtwork.artistContribs': null}),
+    }),
+
+    withRecontextualizedContributionList({
+      list: '#attachedArtwork.artistContribs',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js b/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js
new file mode 100644
index 00000000..a2f954b9
--- /dev/null
+++ b/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js
@@ -0,0 +1,65 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+import withAttachedArtwork from './withAttachedArtwork.js';
+
+function getOutputName({
+  [input.staticValue('property')]: property,
+}) {
+  if (property) {
+    return `#attachedArtwork.${property}`;
+  } else {
+    return '#value';
+  }
+}
+
+export default templateCompositeFrom({
+  annotation: `withPropertyFromAttachedArtwork`,
+
+  inputs: {
+    property: input({type: 'string'}),
+  },
+
+  outputs: inputs => [getOutputName(inputs)],
+
+  steps: () => [
+    {
+      dependencies: [input.staticValue('property')],
+      compute: (continuation, inputs) =>
+        continuation({'#output': getOutputName(inputs)}),
+    },
+
+    withAttachedArtwork(),
+
+    withResultOfAvailabilityCheck({
+      from: '#attachedArtwork',
+    }),
+
+    {
+      dependencies: ['#availability', '#output'],
+      compute: (continuation, {
+        ['#availability']: availability,
+        ['#output']: output,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.raiseOutput({[output]: null})),
+    },
+
+    withPropertyFromObject({
+      object: '#attachedArtwork',
+      property: input('property'),
+    }),
+
+    {
+      dependencies: ['#value', '#output'],
+      compute: (continuation, {
+        ['#value']: value,
+        ['#output']: output,
+      }) =>
+        continuation.raiseOutput({[output]: value}),
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-data/withConstitutedArtwork.js b/src/data/composite/wiki-data/withConstitutedArtwork.js
index 9e260abf..6187d55b 100644
--- a/src/data/composite/wiki-data/withConstitutedArtwork.js
+++ b/src/data/composite/wiki-data/withConstitutedArtwork.js
@@ -6,6 +6,7 @@ export default templateCompositeFrom({
   annotation: `withConstitutedArtwork`,
 
   inputs: {
+    thingProperty: input({type: 'string', acceptsNull: true}),
     dimensionsFromThingProperty: input({type: 'string', acceptsNull: true}),
     fileExtensionFromThingProperty: input({type: 'string', acceptsNull: true}),
     dateFromThingProperty: input({type: 'string', acceptsNull: true}),
@@ -21,6 +22,7 @@ export default templateCompositeFrom({
     {
       dependencies: [
         input.myself(),
+        input('thingProperty'),
         input('dimensionsFromThingProperty'),
         input('fileExtensionFromThingProperty'),
         input('dateFromThingProperty'),
@@ -32,6 +34,7 @@ export default templateCompositeFrom({
 
       compute: (continuation, {
         [input.myself()]: myself,
+        [input('thingProperty')]: thingProperty,
         [input('dimensionsFromThingProperty')]: dimensionsFromThingProperty,
         [input('fileExtensionFromThingProperty')]: fileExtensionFromThingProperty,
         [input('dateFromThingProperty')]: dateFromThingProperty,
@@ -43,6 +46,7 @@ export default templateCompositeFrom({
         ['#constitutedArtwork']:
           Object.assign(new thingConstructors.Artwork, {
             thing: myself,
+            thingProperty,
             dimensionsFromThingProperty,
             fileExtensionFromThingProperty,
             artistContribsFromThingProperty,
diff --git a/src/data/composite/wiki-properties/constitutibleArtwork.js b/src/data/composite/wiki-properties/constitutibleArtwork.js
index 0ee3bfcd..48f4211a 100644
--- a/src/data/composite/wiki-properties/constitutibleArtwork.js
+++ b/src/data/composite/wiki-properties/constitutibleArtwork.js
@@ -17,6 +17,7 @@ const template = templateCompositeFrom({
   compose: false,
 
   inputs: {
+    thingProperty: input({type: 'string', acceptsNull: true}),
     dimensionsFromThingProperty: input({type: 'string', acceptsNull: true}),
     fileExtensionFromThingProperty: input({type: 'string', acceptsNull: true}),
     dateFromThingProperty: input({type: 'string', acceptsNull: true}),
@@ -35,6 +36,7 @@ const template = templateCompositeFrom({
     }),
 
     withConstitutedArtwork({
+      thingProperty: input('thingProperty'),
       dimensionsFromThingProperty: input('dimensionsFromThingProperty'),
       fileExtensionFromThingProperty: input('fileExtensionFromThingProperty'),
       dateFromThingProperty: input('dateFromThingProperty'),
diff --git a/src/data/composite/wiki-properties/constitutibleArtworkList.js b/src/data/composite/wiki-properties/constitutibleArtworkList.js
index 246c08b5..dad3a957 100644
--- a/src/data/composite/wiki-properties/constitutibleArtworkList.js
+++ b/src/data/composite/wiki-properties/constitutibleArtworkList.js
@@ -16,6 +16,7 @@ const template = templateCompositeFrom({
   compose: false,
 
   inputs: {
+    thingProperty: input({type: 'string', acceptsNull: true}),
     dimensionsFromThingProperty: input({type: 'string', acceptsNull: true}),
     fileExtensionFromThingProperty: input({type: 'string', acceptsNull: true}),
     dateFromThingProperty: input({type: 'string', acceptsNull: true}),
@@ -34,6 +35,7 @@ const template = templateCompositeFrom({
     }),
 
     withConstitutedArtwork({
+      thingProperty: input('thingProperty'),
       dimensionsFromThingProperty: input('dimensionsFromThingProperty'),
       fileExtensionFromThingProperty: input('fileExtensionFromThingProperty'),
       dateFromThingProperty: input('dateFromThingProperty'),
diff --git a/src/data/things/album.js b/src/data/things/album.js
index 8a25a8ac..c71b9820 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -516,6 +516,7 @@ export class Album extends Thing {
         property: 'coverArtworks',
         transform:
           parseArtwork({
+            thingProperty: 'coverArtworks',
             dimensionsFromThingProperty: 'coverArtDimensions',
             fileExtensionFromThingProperty: 'coverArtFileExtension',
             dateFromThingProperty: 'coverArtDate',
@@ -531,6 +532,7 @@ export class Album extends Thing {
         transform:
           parseArtwork({
             single: true,
+            thingProperty: 'bannerArtwork',
             dimensionsFromThingProperty: 'bannerDimensions',
             fileExtensionFromThingProperty: 'bannerFileExtension',
             dateFromThingProperty: 'date',
@@ -544,6 +546,7 @@ export class Album extends Thing {
         transform:
           parseArtwork({
             single: true,
+            thingProperty: 'wallpaperArtwork',
             dimensionsFromThingProperty: null,
             fileExtensionFromThingProperty: 'wallpaperFileExtension',
             dateFromThingProperty: 'date',
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 87e1c563..9e329c74 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -213,6 +213,7 @@ export class Artist extends Thing {
         transform:
           parseArtwork({
             single: true,
+            thingProperty: 'avatarArtwork',
             fileExtensionFromThingProperty: 'avatarFileExtension',
           }),
       },
diff --git a/src/data/things/artwork.js b/src/data/things/artwork.js
index 2a97fd6d..3cdb07d0 100644
--- a/src/data/things/artwork.js
+++ b/src/data/things/artwork.js
@@ -24,7 +24,7 @@ import {
   parseDimensions,
 } from '#yaml';
 
-import {withPropertyFromObject} from '#composite/data';
+import {withIndexInList, withPropertyFromObject} from '#composite/data';
 
 import {
   exitWithoutDependency,
@@ -44,6 +44,7 @@ import {
 import {
   contentString,
   directory,
+  flag,
   reverseReferenceList,
   simpleString,
   soupyFind,
@@ -52,7 +53,13 @@ import {
   wikiData,
 } from '#composite/wiki-properties';
 
-import {withDate} from '#composite/things/artwork';
+import {
+  withAttachedArtwork,
+  withContainingArtworkList,
+  withContribsFromAttachedArtwork,
+  withPropertyFromAttachedArtwork,
+  withDate,
+} from '#composite/things/artwork';
 
 export class Artwork extends Thing {
   static [Thing.referenceType] = 'artwork';
@@ -68,6 +75,7 @@ export class Artwork extends Thing {
     }),
 
     thing: thing(),
+    thingProperty: simpleString(),
 
     label: simpleString(),
     source: contentString(),
@@ -152,6 +160,8 @@ export class Artwork extends Thing {
       }),
     ],
 
+    attachAbove: flag(false),
+
     artistContribsFromThingProperty: simpleString(),
     artistContribsArtistProperty: simpleString(),
 
@@ -169,6 +179,12 @@ export class Artwork extends Thing {
         mode: input.value('empty'),
       }),
 
+      withContribsFromAttachedArtwork(),
+
+      exposeDependencyOrContinue({
+        dependency: '#attachedArtwork.artistContribs',
+      }),
+
       exitWithoutDependency({
         dependency: 'artistContribsFromThingProperty',
         value: input.value([]),
@@ -207,6 +223,14 @@ export class Artwork extends Thing {
         mode: input.value('empty'),
       }),
 
+      withPropertyFromAttachedArtwork({
+        property: input.value('artTags'),
+      }),
+
+      exposeDependencyOrContinue({
+        dependency: '#attachedArtwork.artTags',
+      }),
+
       exitWithoutDependency({
         dependency: 'artTagsFromThingProperty',
         value: input.value([]),
@@ -302,6 +326,51 @@ export class Artwork extends Thing {
     referencedByArtworks: reverseReferenceList({
       reverse: soupyReverse.input('artworksWhichReference'),
     }),
+
+    isMainArtwork: [
+      withContainingArtworkList(),
+
+      exitWithoutDependency({
+        dependency: '#containingArtworkList',
+        value: input.value(null),
+      }),
+
+      {
+        dependencies: [input.myself(), '#containingArtworkList'],
+        compute: ({
+          [input.myself()]: myself,
+          ['#containingArtworkList']: list,
+        }) =>
+          list[0] === myself,
+      },
+    ],
+
+    mainArtwork: [
+      withContainingArtworkList(),
+
+      exitWithoutDependency({
+        dependency: '#containingArtworkList',
+        value: input.value(null),
+      }),
+
+      {
+        dependencies: ['#containingArtworkList'],
+        compute: ({'#containingArtworkList': list}) =>
+          list[0],
+      },
+    ],
+
+    attachedArtwork: [
+      withAttachedArtwork(),
+
+      exposeDependency({
+        dependency: '#attachedArtwork',
+      }),
+    ],
+
+    attachingArtworks: reverseReferenceList({
+      reverse: soupyReverse.input('artworksWhichAttach'),
+    }),
   });
 
   static [Thing.yamlDocumentSpec] = {
@@ -322,6 +391,8 @@ export class Artwork extends Thing {
         transform: parseDate,
       },
 
+      'Attach Above': {property: 'attachAbove'},
+
       'Artists': {
         property: 'artistContribs',
         transform: parseContributors,
@@ -358,6 +429,18 @@ export class Artwork extends Thing {
       date: ({artwork}) => artwork.date,
     },
 
+    artworksWhichAttach: {
+      bindTo: 'artworkData',
+
+      referencing: referencingArtwork =>
+        (referencingArtwork.attachAbove
+          ? [referencingArtwork]
+          : []),
+
+      referenced: referencingArtwork =>
+        [referencingArtwork.attachedArtwork],
+    },
+
     artworksWhichFeature: {
       bindTo: 'artworkData',
 
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index dac674dd..a0bcb523 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -230,6 +230,7 @@ export class Flash extends Thing {
         transform:
           parseArtwork({
             single: true,
+            thingProperty: 'coverArtwork',
             fileExtensionFromThingProperty: 'coverArtFileExtension',
             dimensionsFromThingProperty: 'coverArtDimensions',
           }),
diff --git a/src/data/things/track.js b/src/data/things/track.js
index ae7be170..57aaa90d 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -558,6 +558,7 @@ export class Track extends Thing {
         property: 'trackArtworks',
         transform:
           parseArtwork({
+            thingProperty: 'trackArtworks',
             dimensionsFromThingProperty: 'coverArtDimensions',
             fileExtensionFromThingProperty: 'coverArtFileExtension',
             dateFromThingProperty: 'coverArtDate',
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 79602faa..036fe8a7 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -792,6 +792,7 @@ export function parseAnnotatedReferences(entries, {
 
 export function parseArtwork({
   single = false,
+  thingProperty = null,
   dimensionsFromThingProperty = null,
   fileExtensionFromThingProperty = null,
   dateFromThingProperty = null,
@@ -801,6 +802,7 @@ export function parseArtwork({
   referencedArtworksFromThingProperty = null,
 }) {
   const provide = {
+    thingProperty,
     dimensionsFromThingProperty,
     fileExtensionFromThingProperty,
     dateFromThingProperty,