« 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/composite
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/composite')
-rw-r--r--src/data/composite/data/index.js1
-rw-r--r--src/data/composite/data/withLengthOfList.js54
-rw-r--r--src/data/composite/things/album/index.js2
-rw-r--r--src/data/composite/things/album/withCoverArtDate.js (renamed from src/data/composite/wiki-data/withCoverArtDate.js)13
-rw-r--r--src/data/composite/things/artwork/withContribsFromAttachedArtwork.js1
-rw-r--r--src/data/composite/things/commentary-entry/index.js1
-rw-r--r--src/data/composite/things/content/contentArtists.js40
-rw-r--r--src/data/composite/things/content/hasAnnotationPart.js25
-rw-r--r--src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js60
-rw-r--r--src/data/composite/things/content/index.js7
-rw-r--r--src/data/composite/things/content/withAnnotationParts.js93
-rw-r--r--src/data/composite/things/content/withHasAnnotationPart.js43
-rw-r--r--src/data/composite/things/content/withSourceText.js53
-rw-r--r--src/data/composite/things/content/withSourceURLs.js62
-rw-r--r--src/data/composite/things/content/withWebArchiveDate.js (renamed from src/data/composite/things/commentary-entry/withWebArchiveDate.js)0
-rw-r--r--src/data/composite/things/contribution/thingPropertyMatches.js1
-rw-r--r--src/data/composite/things/track-section/withContinueCountingFrom.js2
-rw-r--r--src/data/composite/things/track/trackAdditionalNameList.js38
-rw-r--r--src/data/composite/things/track/withAllReleases.js1
-rw-r--r--src/data/composite/things/track/withAlwaysReferenceByDirectory.js1
-rw-r--r--src/data/composite/things/track/withOtherReleases.js3
-rw-r--r--src/data/composite/wiki-data/exitWithoutArtwork.js45
-rw-r--r--src/data/composite/wiki-data/index.js6
-rw-r--r--src/data/composite/wiki-data/splitContentNodesAround.js87
-rw-r--r--src/data/composite/wiki-data/withConstitutedArtwork.js1
-rw-r--r--src/data/composite/wiki-data/withContentNodes.js25
-rw-r--r--src/data/composite/wiki-data/withHasArtwork.js (renamed from src/data/composite/things/album/withHasCoverArt.js)59
-rw-r--r--src/data/composite/wiki-data/withResolvedSeriesList.js130
-rw-r--r--src/data/composite/wiki-properties/additionalFiles.js30
-rw-r--r--src/data/composite/wiki-properties/additionalNameList.js14
-rw-r--r--src/data/composite/wiki-properties/index.js3
-rw-r--r--src/data/composite/wiki-properties/referencedArtworkList.js1
-rw-r--r--src/data/composite/wiki-properties/seriesList.js31
33 files changed, 653 insertions, 280 deletions
diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js
index 46a3dc81..05b59445 100644
--- a/src/data/composite/data/index.js
+++ b/src/data/composite/data/index.js
@@ -20,6 +20,7 @@ export {default as withMappedList} from './withMappedList.js';
 export {default as withSortedList} from './withSortedList.js';
 export {default as withStretchedList} from './withStretchedList.js';
 
+export {default as withLengthOfList} from './withLengthOfList.js';
 export {default as withPropertyFromList} from './withPropertyFromList.js';
 export {default as withPropertiesFromList} from './withPropertiesFromList.js';
 
diff --git a/src/data/composite/data/withLengthOfList.js b/src/data/composite/data/withLengthOfList.js
new file mode 100644
index 00000000..e67aa887
--- /dev/null
+++ b/src/data/composite/data/withLengthOfList.js
@@ -0,0 +1,54 @@
+import {input, templateCompositeFrom} from '#composite';
+
+function getOutputName({
+  [input.staticDependency('list')]: list,
+}) {
+  if (list && list.startsWith('#')) {
+    return `${list}.length`;
+  } else if (list) {
+    return `#${list}.length`;
+  } else {
+    return '#length';
+  }
+}
+
+export default templateCompositeFrom({
+  annotation: `withMappedList`,
+
+  inputs: {
+    list: input({type: 'array'}),
+  },
+
+  outputs: inputs => [getOutputName(inputs)],
+
+  steps: () => [
+    {
+      dependencies: [input.staticDependency('list')],
+      compute: (continuation, inputs) =>
+        continuation({'#output': getOutputName(inputs)}),
+    },
+
+    {
+      dependencies: [input('list')],
+      compute: (continuation, {
+        [input('list')]: list,
+      }) => continuation({
+        ['#value']:
+          (list === null
+            ? null
+            : list.length),
+      }),
+    },
+
+    {
+      dependencies: ['#output', '#value'],
+
+      compute: (continuation, {
+        ['#output']: output,
+        ['#value']: value,
+      }) => continuation({
+        [output]: value,
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js
index dfc6864f..de1d37c3 100644
--- a/src/data/composite/things/album/index.js
+++ b/src/data/composite/things/album/index.js
@@ -1,2 +1,2 @@
-export {default as withHasCoverArt} from './withHasCoverArt.js';
+export {default as withCoverArtDate} from './withCoverArtDate.js';
 export {default as withTracks} from './withTracks.js';
diff --git a/src/data/composite/wiki-data/withCoverArtDate.js b/src/data/composite/things/album/withCoverArtDate.js
index a114d5ff..978f566a 100644
--- a/src/data/composite/wiki-data/withCoverArtDate.js
+++ b/src/data/composite/things/album/withCoverArtDate.js
@@ -2,8 +2,7 @@ import {input, templateCompositeFrom} from '#composite';
 import {isDate} from '#validators';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
-
-import withResolvedContribs from './withResolvedContribs.js';
+import {withHasArtwork} from '#composite/wiki-data';
 
 export default templateCompositeFrom({
   annotation: `withCoverArtDate`,
@@ -19,14 +18,14 @@ export default templateCompositeFrom({
   outputs: ['#coverArtDate'],
 
   steps: () => [
-    withResolvedContribs({
-      from: 'coverArtistContribs',
-      date: input.value(null),
+    withHasArtwork({
+      contribs: 'coverArtistContribs',
+      artworks: 'coverArtworks',
     }),
 
     raiseOutputWithoutDependency({
-      dependency: '#resolvedContribs',
-      mode: input.value('empty'),
+      dependency: '#hasArtwork',
+      mode: input.value('falsy'),
       output: input.value({'#coverArtDate': null}),
     }),
 
diff --git a/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js
index 36abb3fe..e9425c95 100644
--- a/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js
+++ b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js
@@ -1,7 +1,6 @@
 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';
diff --git a/src/data/composite/things/commentary-entry/index.js b/src/data/composite/things/commentary-entry/index.js
deleted file mode 100644
index 091bae1a..00000000
--- a/src/data/composite/things/commentary-entry/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export {default as withWebArchiveDate} from './withWebArchiveDate.js';
diff --git a/src/data/composite/things/content/contentArtists.js b/src/data/composite/things/content/contentArtists.js
new file mode 100644
index 00000000..8d5db5a5
--- /dev/null
+++ b/src/data/composite/things/content/contentArtists.js
@@ -0,0 +1,40 @@
+import {input, templateCompositeFrom} from '#composite';
+import {validateReferenceList} from '#validators';
+
+import {exitWithoutDependency, exposeDependency}
+  from '#composite/control-flow';
+import {withResolvedReferenceList} from '#composite/wiki-data';
+import {soupyFind} from '#composite/wiki-properties';
+
+import withExpressedOrImplicitArtistReferences
+  from './helpers/withExpressedOrImplicitArtistReferences.js';
+
+export default templateCompositeFrom({
+  annotation: `contentArtists`,
+
+  compose: false,
+
+  update: {
+    validate: validateReferenceList('artist'),
+  },
+
+  steps: () => [
+    withExpressedOrImplicitArtistReferences({
+      from: input.updateValue(),
+    }),
+
+    exitWithoutDependency({
+      dependency: '#artistReferences',
+      value: input.value([]),
+    }),
+
+    withResolvedReferenceList({
+      list: '#artistReferences',
+      find: soupyFind.input('artist'),
+    }),
+
+    exposeDependency({
+      dependency: '#resolvedReferenceList',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/content/hasAnnotationPart.js b/src/data/composite/things/content/hasAnnotationPart.js
new file mode 100644
index 00000000..83d175e3
--- /dev/null
+++ b/src/data/composite/things/content/hasAnnotationPart.js
@@ -0,0 +1,25 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {exposeDependency} from '#composite/control-flow';
+
+import withHasAnnotationPart from './withHasAnnotationPart.js';
+
+export default templateCompositeFrom({
+  annotation: `hasAnnotationPart`,
+
+  compose: false,
+
+  inputs: {
+    part: input({type: 'string'}),
+  },
+
+  steps: () => [
+    withHasAnnotationPart({
+      part: input('part'),
+    }),
+
+    exposeDependency({
+      dependency: '#hasAnnotationPart',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js b/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js
new file mode 100644
index 00000000..62799d43
--- /dev/null
+++ b/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js
@@ -0,0 +1,60 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withFilteredList, withMappedList} from '#composite/data';
+import {withContentNodes} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `withExpressedOrImplicitArtistReferences`,
+
+  inputs: {
+    from: input({type: 'array', acceptsNull: true}),
+  },
+
+  outputs: ['#artistReferences'],
+
+  steps: () => [
+    {
+      dependencies: [input('from')],
+      compute: (continuation, {
+        [input('from')]: expressedArtistReferences,
+      }) =>
+        (expressedArtistReferences
+          ? continuation.raiseOutput({'#artistReferences': expressedArtistReferences})
+          : continuation()),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: 'artistText',
+      output: input.value({'#artistReferences': null}),
+    }),
+
+    withContentNodes({
+      from: 'artistText',
+    }),
+
+    withMappedList({
+      list: '#contentNodes',
+      map: input.value(node =>
+        node.type === 'tag' &&
+        node.data.replacerKey?.data === 'artist'),
+    }).outputs({
+      '#mappedList': '#artistTagFilter',
+    }),
+
+    withFilteredList({
+      list: '#contentNodes',
+      filter: '#artistTagFilter',
+    }).outputs({
+      '#filteredList': '#artistTags',
+    }),
+
+    withMappedList({
+      list: '#artistTags',
+      map: input.value(node =>
+        node.data.replacerValue[0].data),
+    }).outputs({
+      '#mappedList': '#artistReferences',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/content/index.js b/src/data/composite/things/content/index.js
new file mode 100644
index 00000000..4176337d
--- /dev/null
+++ b/src/data/composite/things/content/index.js
@@ -0,0 +1,7 @@
+export {default as contentArtists} from './contentArtists.js';
+export {default as hasAnnotationPart} from './hasAnnotationPart.js';
+export {default as withAnnotationParts} from './withAnnotationParts.js';
+export {default as withHasAnnotationPart} from './withHasAnnotationPart.js';
+export {default as withSourceText} from './withSourceText.js';
+export {default as withSourceURLs} from './withSourceURLs.js';
+export {default as withWebArchiveDate} from './withWebArchiveDate.js';
diff --git a/src/data/composite/things/content/withAnnotationParts.js b/src/data/composite/things/content/withAnnotationParts.js
new file mode 100644
index 00000000..6311b57a
--- /dev/null
+++ b/src/data/composite/things/content/withAnnotationParts.js
@@ -0,0 +1,93 @@
+import {input, templateCompositeFrom} from '#composite';
+import {transposeArrays} from '#sugar';
+import {is} from '#validators';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromList} from '#composite/data';
+import {splitContentNodesAround, withContentNodes} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `withAnnotationParts`,
+
+  inputs: {
+    mode: input({
+      validate: is('strings', 'nodes'),
+    }),
+  },
+
+  outputs: ['#annotationParts'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: 'annotation',
+      output: input.value({'#annotationParts': []}),
+    }),
+
+    withContentNodes({
+      from: 'annotation',
+    }),
+
+    splitContentNodesAround({
+      nodes: '#contentNodes',
+      around: input.value(/, */g),
+    }),
+
+    {
+      dependencies: ['#contentNodeLists', input('mode')],
+      compute: (continuation, {
+        ['#contentNodeLists']: nodeLists,
+        [input('mode')]: mode,
+      }) =>
+        (mode === 'nodes'
+          ? continuation.raiseOutput({'#annotationParts': nodeLists})
+          : continuation()),
+    },
+
+    {
+      dependencies: ['#contentNodeLists'],
+
+      compute: (continuation, {
+        ['#contentNodeLists']: nodeLists,
+      }) => continuation({
+        ['#firstNodes']:
+          nodeLists.map(list => list.at(0)),
+
+        ['#lastNodes']:
+          nodeLists.map(list => list.at(-1)),
+      }),
+    },
+
+    withPropertyFromList({
+      list: '#firstNodes',
+      property: input.value('i'),
+    }).outputs({
+      '#firstNodes.i': '#startIndices',
+    }),
+
+    withPropertyFromList({
+      list: '#lastNodes',
+      property: input.value('iEnd'),
+    }).outputs({
+      '#lastNodes.iEnd': '#endIndices',
+    }),
+
+    {
+      dependencies: [
+        'annotation',
+        '#startIndices',
+        '#endIndices',
+      ],
+
+      compute: (continuation, {
+        ['annotation']: annotation,
+        ['#startIndices']: startIndices,
+        ['#endIndices']: endIndices,
+      }) => continuation({
+        ['#annotationParts']:
+          transposeArrays([startIndices, endIndices])
+            .map(([start, end]) =>
+              annotation.slice(start, end)),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/content/withHasAnnotationPart.js b/src/data/composite/things/content/withHasAnnotationPart.js
new file mode 100644
index 00000000..4af554f3
--- /dev/null
+++ b/src/data/composite/things/content/withHasAnnotationPart.js
@@ -0,0 +1,43 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+
+import withAnnotationParts from './withAnnotationParts.js';
+
+export default templateCompositeFrom({
+  annotation: `withHasAnnotationPart`,
+
+  inputs: {
+    part: input({type: 'string'}),
+  },
+
+  outputs: ['#hasAnnotationPart'],
+
+  steps: () => [
+    withAnnotationParts({
+      mode: input.value('strings'),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#annotationParts',
+      output: input.value({'#hasAnnotationPart': false}),
+    }),
+
+    {
+      dependencies: [
+        input('part'),
+        '#annotationParts',
+      ],
+
+      compute: (continuation, {
+        [input('part')]: search,
+        ['#annotationParts']: parts,
+      }) => continuation({
+        ['#hasAnnotationPart']:
+          parts.some(part =>
+            part.toLowerCase() ===
+            search.toLowerCase()),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/content/withSourceText.js b/src/data/composite/things/content/withSourceText.js
new file mode 100644
index 00000000..292306b7
--- /dev/null
+++ b/src/data/composite/things/content/withSourceText.js
@@ -0,0 +1,53 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+
+import withAnnotationParts from './withAnnotationParts.js';
+
+export default templateCompositeFrom({
+  annotation: `withSourceText`,
+
+  outputs: ['#sourceText'],
+
+  steps: () => [
+    withAnnotationParts({
+      mode: input.value('nodes'),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#annotationParts',
+      output: input.value({'#sourceText': null}),
+    }),
+
+    {
+      dependencies: ['#annotationParts'],
+      compute: (continuation, {
+        ['#annotationParts']: annotationParts,
+      }) => continuation({
+        ['#firstPartWithExternalLink']:
+          annotationParts
+            .find(nodes => nodes
+              .some(node => node.type === 'external-link')) ??
+          null,
+      }),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: '#firstPartWithExternalLink',
+      output: input.value({'#sourceText': null}),
+    }),
+
+    {
+      dependencies: ['annotation', '#firstPartWithExternalLink'],
+      compute: (continuation, {
+        ['annotation']: annotation,
+        ['#firstPartWithExternalLink']: nodes,
+      }) => continuation({
+        ['#sourceText']:
+          annotation.slice(
+            nodes.at(0).i,
+            nodes.at(-1).iEnd),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/content/withSourceURLs.js b/src/data/composite/things/content/withSourceURLs.js
new file mode 100644
index 00000000..f85ff9ea
--- /dev/null
+++ b/src/data/composite/things/content/withSourceURLs.js
@@ -0,0 +1,62 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withFilteredList, withMappedList} from '#composite/data';
+
+import withAnnotationParts from './withAnnotationParts.js';
+
+export default templateCompositeFrom({
+  annotation: `withSourceURLs`,
+
+  outputs: ['#sourceURLs'],
+
+  steps: () => [
+    withAnnotationParts({
+      mode: input.value('nodes'),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#annotationParts',
+      output: input.value({'#sourceURLs': []}),
+    }),
+
+    {
+      dependencies: ['#annotationParts'],
+      compute: (continuation, {
+        ['#annotationParts']: annotationParts,
+      }) => continuation({
+        ['#firstPartWithExternalLink']:
+          annotationParts
+            .find(nodes => nodes
+              .some(node => node.type === 'external-link')) ??
+          null,
+      }),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: '#firstPartWithExternalLink',
+      output: input.value({'#sourceURLs': []}),
+    }),
+
+    withMappedList({
+      list: '#firstPartWithExternalLink',
+      map: input.value(node => node.type === 'external-link'),
+    }).outputs({
+      '#mappedList': '#externalLinkFilter',
+    }),
+
+    withFilteredList({
+      list: '#firstPartWithExternalLink',
+      filter: '#externalLinkFilter',
+    }).outputs({
+      '#filteredList': '#externalLinks',
+    }),
+
+    withMappedList({
+      list: '#externalLinks',
+      map: input.value(node => node.data.href),
+    }).outputs({
+      '#mappedList': '#sourceURLs',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/commentary-entry/withWebArchiveDate.js b/src/data/composite/things/content/withWebArchiveDate.js
index 3aaa4f64..3aaa4f64 100644
--- a/src/data/composite/things/commentary-entry/withWebArchiveDate.js
+++ b/src/data/composite/things/content/withWebArchiveDate.js
diff --git a/src/data/composite/things/contribution/thingPropertyMatches.js b/src/data/composite/things/contribution/thingPropertyMatches.js
index 1e9019b8..a678c3f5 100644
--- a/src/data/composite/things/contribution/thingPropertyMatches.js
+++ b/src/data/composite/things/contribution/thingPropertyMatches.js
@@ -1,7 +1,6 @@
 import {input, templateCompositeFrom} from '#composite';
 
 import {exitWithoutDependency} from '#composite/control-flow';
-import {withPropertyFromObject} from '#composite/data';
 
 export default templateCompositeFrom({
   annotation: `thingPropertyMatches`,
diff --git a/src/data/composite/things/track-section/withContinueCountingFrom.js b/src/data/composite/things/track-section/withContinueCountingFrom.js
index e034b7a5..0ca52b6c 100644
--- a/src/data/composite/things/track-section/withContinueCountingFrom.js
+++ b/src/data/composite/things/track-section/withContinueCountingFrom.js
@@ -1,4 +1,4 @@
-import {input, templateCompositeFrom} from '#composite';
+import {templateCompositeFrom} from '#composite';
 
 import withStartCountingFrom from './withStartCountingFrom.js';
 
diff --git a/src/data/composite/things/track/trackAdditionalNameList.js b/src/data/composite/things/track/trackAdditionalNameList.js
deleted file mode 100644
index 65a2263d..00000000
--- a/src/data/composite/things/track/trackAdditionalNameList.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// Compiles additional names from various sources.
-
-import {input, templateCompositeFrom} from '#composite';
-import {isAdditionalNameList} from '#validators';
-
-import withInferredAdditionalNames from './withInferredAdditionalNames.js';
-import withSharedAdditionalNames from './withSharedAdditionalNames.js';
-
-export default templateCompositeFrom({
-  annotation: `trackAdditionalNameList`,
-
-  compose: false,
-
-  update: {validate: isAdditionalNameList},
-
-  steps: () => [
-    withInferredAdditionalNames(),
-    withSharedAdditionalNames(),
-
-    {
-      dependencies: [
-        '#inferredAdditionalNames',
-        '#sharedAdditionalNames',
-        input.updateValue(),
-      ],
-
-      compute: ({
-        ['#inferredAdditionalNames']: inferredAdditionalNames,
-        ['#sharedAdditionalNames']: sharedAdditionalNames,
-        [input.updateValue()]: providedAdditionalNames,
-      }) => [
-        ...providedAdditionalNames ?? [],
-        ...sharedAdditionalNames,
-        ...inferredAdditionalNames,
-      ],
-    },
-  ],
-});
diff --git a/src/data/composite/things/track/withAllReleases.js b/src/data/composite/things/track/withAllReleases.js
index b93bf753..891db102 100644
--- a/src/data/composite/things/track/withAllReleases.js
+++ b/src/data/composite/things/track/withAllReleases.js
@@ -8,7 +8,6 @@
 import {input, templateCompositeFrom} from '#composite';
 import {sortByDate} from '#sort';
 
-import {exitWithoutDependency} from '#composite/control-flow';
 import {withPropertyFromObject} from '#composite/data';
 
 import withMainRelease from './withMainRelease.js';
diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
index 60faeaf4..87edf21e 100644
--- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
+++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
@@ -9,7 +9,6 @@ import {isBoolean} from '#validators';
 
 import {withPropertyFromObject} from '#composite/data';
 import {withResolvedReference} from '#composite/wiki-data';
-import {soupyFind} from '#composite/wiki-properties';
 
 import {
   exitWithoutDependency,
diff --git a/src/data/composite/things/track/withOtherReleases.js b/src/data/composite/things/track/withOtherReleases.js
index 0639742f..bb3e8983 100644
--- a/src/data/composite/things/track/withOtherReleases.js
+++ b/src/data/composite/things/track/withOtherReleases.js
@@ -3,9 +3,6 @@
 
 import {input, templateCompositeFrom} from '#composite';
 
-import {exitWithoutDependency} from '#composite/control-flow';
-import {withPropertyFromObject} from '#composite/data';
-
 import withAllReleases from './withAllReleases.js';
 
 export default templateCompositeFrom({
diff --git a/src/data/composite/wiki-data/exitWithoutArtwork.js b/src/data/composite/wiki-data/exitWithoutArtwork.js
new file mode 100644
index 00000000..8e799fda
--- /dev/null
+++ b/src/data/composite/wiki-data/exitWithoutArtwork.js
@@ -0,0 +1,45 @@
+import {input, templateCompositeFrom} from '#composite';
+import {isContributionList, isThing, strictArrayOf} from '#validators';
+
+import {exitWithoutDependency} from '#composite/control-flow';
+
+import withHasArtwork from './withHasArtwork.js';
+
+export default templateCompositeFrom({
+  annotation: `exitWithoutArtwork`,
+
+  inputs: {
+    contribs: input({
+      validate: isContributionList,
+      defaultValue: null,
+    }),
+
+    artwork: input({
+      validate: isThing,
+      defaultValue: null,
+    }),
+
+    artworks: input({
+      validate: strictArrayOf(isThing),
+      defaultValue: null,
+    }),
+
+    value: input({
+      defaultValue: null,
+    }),
+  },
+
+  steps: () => [
+    withHasArtwork({
+      contribs: input('contribs'),
+      artwork: input('artwork'),
+      artworks: input('artworks'),
+    }),
+
+    exitWithoutDependency({
+      dependency: '#hasArtwork',
+      mode: input.value('falsy'),
+      value: input('value'),
+    }),
+  ],
+});
diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js
index 005c68c0..3206575b 100644
--- a/src/data/composite/wiki-data/index.js
+++ b/src/data/composite/wiki-data/index.js
@@ -5,24 +5,26 @@
 //
 
 export {default as exitWithoutContribs} from './exitWithoutContribs.js';
+export {default as exitWithoutArtwork} from './exitWithoutArtwork.js';
 export {default as gobbleSoupyFind} from './gobbleSoupyFind.js';
 export {default as gobbleSoupyReverse} from './gobbleSoupyReverse.js';
 export {default as inputNotFoundMode} from './inputNotFoundMode.js';
 export {default as inputSoupyFind} from './inputSoupyFind.js';
 export {default as inputSoupyReverse} from './inputSoupyReverse.js';
 export {default as inputWikiData} from './inputWikiData.js';
+export {default as splitContentNodesAround} from './splitContentNodesAround.js';
 export {default as withClonedThings} from './withClonedThings.js';
 export {default as withConstitutedArtwork} from './withConstitutedArtwork.js';
+export {default as withContentNodes} from './withContentNodes.js';
 export {default as withContributionListSums} from './withContributionListSums.js';
-export {default as withCoverArtDate} from './withCoverArtDate.js';
 export {default as withDirectory} from './withDirectory.js';
+export {default as withHasArtwork} from './withHasArtwork.js';
 export {default as withRecontextualizedContributionList} from './withRecontextualizedContributionList.js';
 export {default as withRedatedContributionList} from './withRedatedContributionList.js';
 export {default as withResolvedAnnotatedReferenceList} from './withResolvedAnnotatedReferenceList.js';
 export {default as withResolvedContribs} from './withResolvedContribs.js';
 export {default as withResolvedReference} from './withResolvedReference.js';
 export {default as withResolvedReferenceList} from './withResolvedReferenceList.js';
-export {default as withResolvedSeriesList} from './withResolvedSeriesList.js';
 export {default as withReverseReferenceList} from './withReverseReferenceList.js';
 export {default as withThingsSortedAlphabetically} from './withThingsSortedAlphabetically.js';
 export {default as withUniqueReferencingThing} from './withUniqueReferencingThing.js';
diff --git a/src/data/composite/wiki-data/splitContentNodesAround.js b/src/data/composite/wiki-data/splitContentNodesAround.js
new file mode 100644
index 00000000..6648d8e1
--- /dev/null
+++ b/src/data/composite/wiki-data/splitContentNodesAround.js
@@ -0,0 +1,87 @@
+import {input, templateCompositeFrom} from '#composite';
+import {splitContentNodesAround} from '#replacer';
+import {anyOf, isFunction, validateInstanceOf} from '#validators';
+
+import {withFilteredList, withMappedList, withUnflattenedList}
+  from '#composite/data';
+
+export default templateCompositeFrom({
+  annotation: `splitContentNodesAround`,
+
+  inputs: {
+    nodes: input({type: 'array'}),
+
+    around: input({
+      validate:
+        anyOf(isFunction, validateInstanceOf(RegExp)),
+    }),
+  },
+
+  outputs: ['#contentNodeLists'],
+
+  steps: () => [
+    {
+      dependencies: [input('nodes'), input('around')],
+
+      compute: (continuation, {
+        [input('nodes')]: nodes,
+        [input('around')]: splitter,
+      }) => continuation({
+        ['#nodes']:
+          Array.from(splitContentNodesAround(nodes, splitter)),
+      }),
+    },
+
+    withMappedList({
+      list: '#nodes',
+      map: input.value(node => node.type === 'separator'),
+    }).outputs({
+      '#mappedList': '#separatorFilter',
+    }),
+
+    withMappedList({
+      list: '#separatorFilter',
+      filter: '#separatorFilter',
+      map: input.value((_node, index) => index),
+    }),
+
+    withFilteredList({
+      list: '#mappedList',
+      filter: '#separatorFilter',
+    }).outputs({
+      '#filteredList': '#separatorIndices',
+    }),
+
+    {
+      dependencies: ['#nodes', '#separatorFilter'],
+
+      compute: (continuation, {
+        ['#nodes']: nodes,
+        ['#separatorFilter']: separatorFilter,
+      }) => continuation({
+        ['#nodes']:
+          nodes.map((node, index) =>
+            (separatorFilter[index]
+              ? null
+              : node)),
+      }),
+    },
+
+    {
+      dependencies: ['#separatorIndices'],
+      compute: (continuation, {
+        ['#separatorIndices']: separatorIndices,
+      }) => continuation({
+        ['#unflattenIndices']:
+          [0, ...separatorIndices],
+      }),
+    },
+
+    withUnflattenedList({
+      list: '#nodes',
+      indices: '#unflattenIndices',
+    }).outputs({
+      '#unflattenedList': '#contentNodeLists',
+    }),
+  ],
+});
diff --git a/src/data/composite/wiki-data/withConstitutedArtwork.js b/src/data/composite/wiki-data/withConstitutedArtwork.js
index 6187d55b..28d719e2 100644
--- a/src/data/composite/wiki-data/withConstitutedArtwork.js
+++ b/src/data/composite/wiki-data/withConstitutedArtwork.js
@@ -1,6 +1,5 @@
 import {input, templateCompositeFrom} from '#composite';
 import thingConstructors from '#things';
-import {isContributionList} from '#validators';
 
 export default templateCompositeFrom({
   annotation: `withConstitutedArtwork`,
diff --git a/src/data/composite/wiki-data/withContentNodes.js b/src/data/composite/wiki-data/withContentNodes.js
new file mode 100644
index 00000000..d014d43b
--- /dev/null
+++ b/src/data/composite/wiki-data/withContentNodes.js
@@ -0,0 +1,25 @@
+import {input, templateCompositeFrom} from '#composite';
+import {parseContentNodes} from '#replacer';
+
+export default templateCompositeFrom({
+  annotation: `withContentNodes`,
+
+  inputs: {
+    from: input({type: 'string', acceptsNull: false}),
+  },
+
+  outputs: ['#contentNodes'],
+
+  steps: () => [
+    {
+      dependencies: [input('from')],
+
+      compute: (continuation, {
+        [input('from')]: string,
+      }) => continuation({
+        ['#contentNodes']:
+          parseContentNodes(string),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/album/withHasCoverArt.js b/src/data/composite/wiki-data/withHasArtwork.js
index fd3f2894..9c22f439 100644
--- a/src/data/composite/things/album/withHasCoverArt.js
+++ b/src/data/composite/wiki-data/withHasArtwork.js
@@ -1,7 +1,5 @@
-// TODO: This shouldn't be coded as an Album-specific thing,
-// or even really to do with cover artworks in particular, either.
-
 import {input, templateCompositeFrom} from '#composite';
+import {isContributionList, isThing, strictArrayOf} from '#validators';
 
 import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck}
   from '#composite/control-flow';
@@ -9,13 +7,30 @@ import {fillMissingListItems, withFlattenedList, withPropertyFromList}
   from '#composite/data';
 
 export default templateCompositeFrom({
-  annotation: 'withHasCoverArt',
+  annotation: 'withHasArtwork',
+
+  inputs: {
+    contribs: input({
+      validate: isContributionList,
+      defaultValue: null,
+    }),
+
+    artwork: input({
+      validate: isThing,
+      defaultValue: null,
+    }),
+
+    artworks: input({
+      validate: strictArrayOf(isThing),
+      defaultValue: null,
+    }),
+  },
 
-  outputs: ['#hasCoverArt'],
+  outputs: ['#hasArtwork'],
 
   steps: () => [
     withResultOfAvailabilityCheck({
-      from: 'coverArtistContribs',
+      from: input('contribs'),
       mode: input.value('empty'),
     }),
 
@@ -26,19 +41,37 @@ export default templateCompositeFrom({
       }) =>
         (availability
           ? continuation.raiseOutput({
-              ['#hasCoverArt']: true,
+              ['#hasArtwork']: true,
             })
           : continuation()),
     },
 
+    {
+      dependencies: [input('artwork'), input('artworks')],
+      compute: (continuation, {
+        [input('artwork')]: artwork,
+        [input('artworks')]: artworks,
+      }) =>
+        continuation({
+          ['#artworks']:
+            (artwork && artworks
+              ? [artwork, ...artworks]
+           : artwork
+              ? [artwork]
+           : artworks
+              ? artworks
+              : []),
+        }),
+    },
+
     raiseOutputWithoutDependency({
-      dependency: 'coverArtworks',
+      dependency: '#artworks',
       mode: input.value('empty'),
-      output: input.value({'#hasCoverArt': false}),
+      output: input.value({'#hasArtwork': false}),
     }),
 
     withPropertyFromList({
-      list: 'coverArtworks',
+      list: '#artworks',
       property: input.value('artistContribs'),
       internal: input.value(true),
     }),
@@ -46,19 +79,19 @@ export default templateCompositeFrom({
     // Since we're getting the update value for each artwork's artistContribs,
     // it may not be set at all, and in that case won't be exposing as [].
     fillMissingListItems({
-      list: '#coverArtworks.artistContribs',
+      list: '#artworks.artistContribs',
       fill: input.value([]),
     }),
 
     withFlattenedList({
-      list: '#coverArtworks.artistContribs',
+      list: '#artworks.artistContribs',
     }),
 
     withResultOfAvailabilityCheck({
       from: '#flattenedList',
       mode: input.value('empty'),
     }).outputs({
-      '#availability': '#hasCoverArt',
+      '#availability': '#hasArtwork',
     }),
   ],
 });
diff --git a/src/data/composite/wiki-data/withResolvedSeriesList.js b/src/data/composite/wiki-data/withResolvedSeriesList.js
deleted file mode 100644
index deaab466..00000000
--- a/src/data/composite/wiki-data/withResolvedSeriesList.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import {input, templateCompositeFrom} from '#composite';
-import {stitchArrays} from '#sugar';
-import {isSeriesList, validateThing} from '#validators';
-
-import {raiseOutputWithoutDependency} from '#composite/control-flow';
-
-import {
-  fillMissingListItems,
-  withFlattenedList,
-  withUnflattenedList,
-  withPropertiesFromList,
-} from '#composite/data';
-
-import inputSoupyFind from './inputSoupyFind.js';
-import withResolvedReferenceList from './withResolvedReferenceList.js';
-
-export default templateCompositeFrom({
-  annotation: `withResolvedSeriesList`,
-
-  inputs: {
-    group: input({
-      validate: validateThing({referenceType: 'group'}),
-    }),
-
-    list: input({
-      validate: isSeriesList,
-      acceptsNull: true,
-    }),
-  },
-
-  outputs: ['#resolvedSeriesList'],
-
-  steps: () => [
-    raiseOutputWithoutDependency({
-      dependency: input('list'),
-      mode: input.value('empty'),
-      output: input.value({
-        ['#resolvedSeriesList']: [],
-      }),
-    }),
-
-    withPropertiesFromList({
-      list: input('list'),
-      prefix: input.value('#serieses'),
-      properties: input.value([
-        'name',
-        'description',
-        'albums',
-
-        'showAlbumArtists',
-      ]),
-    }),
-
-    fillMissingListItems({
-      list: '#serieses.albums',
-      fill: input.value([]),
-    }),
-
-    withFlattenedList({
-      list: '#serieses.albums',
-    }),
-
-    withResolvedReferenceList({
-      list: '#flattenedList',
-      find: inputSoupyFind.input('album'),
-      notFoundMode: input.value('null'),
-    }),
-
-    withUnflattenedList({
-      list: '#resolvedReferenceList',
-    }).outputs({
-      '#unflattenedList': '#serieses.albums',
-    }),
-
-    fillMissingListItems({
-      list: '#serieses.description',
-      fill: input.value(null),
-    }),
-
-    fillMissingListItems({
-      list: '#serieses.showAlbumArtists',
-      fill: input.value(null),
-    }),
-
-    {
-      dependencies: [
-        '#serieses.name',
-        '#serieses.description',
-        '#serieses.albums',
-
-        '#serieses.showAlbumArtists',
-      ],
-
-      compute: (continuation, {
-        ['#serieses.name']: name,
-        ['#serieses.description']: description,
-        ['#serieses.albums']: albums,
-
-        ['#serieses.showAlbumArtists']: showAlbumArtists,
-      }) => continuation({
-        ['#seriesProperties']:
-          stitchArrays({
-            name,
-            description,
-            albums,
-
-            showAlbumArtists,
-          }).map(properties => ({
-              ...properties,
-              group: input
-            }))
-      }),
-    },
-
-    {
-      dependencies: ['#seriesProperties', input('group')],
-      compute: (continuation, {
-        ['#seriesProperties']: seriesProperties,
-        [input('group')]: group,
-      }) => continuation({
-        ['#resolvedSeriesList']:
-          seriesProperties
-            .map(properties => ({
-              ...properties,
-              group,
-            })),
-      }),
-    },
-  ],
-});
diff --git a/src/data/composite/wiki-properties/additionalFiles.js b/src/data/composite/wiki-properties/additionalFiles.js
deleted file mode 100644
index 6760527a..00000000
--- a/src/data/composite/wiki-properties/additionalFiles.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// This is a somewhat more involved data structure - it's for additional
-// or "bonus" files associated with albums or tracks (or anything else).
-// It's got this form:
-//
-//   [
-//     {title: 'Booklet', files: ['Booklet.pdf']},
-//     {
-//       title: 'Wallpaper',
-//       description: 'Cool Wallpaper!',
-//       files: ['1440x900.png', '1920x1080.png']
-//     },
-//     {title: 'Alternate Covers', description: null, files: [...]},
-//     ...
-//   ]
-//
-
-import {isAdditionalFileList} from '#validators';
-
-// TODO: Not templateCompositeFrom.
-
-export default function() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isAdditionalFileList},
-    expose: {
-      transform: (additionalFiles) =>
-        additionalFiles ?? [],
-    },
-  };
-}
diff --git a/src/data/composite/wiki-properties/additionalNameList.js b/src/data/composite/wiki-properties/additionalNameList.js
deleted file mode 100644
index c5971d4a..00000000
--- a/src/data/composite/wiki-properties/additionalNameList.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// A list of additional names! These can be used for a variety of purposes,
-// e.g. providing extra searchable titles, localizations, romanizations or
-// original titles, and so on. Each item has a name and, optionally, a
-// descriptive annotation.
-
-import {isAdditionalNameList} from '#validators';
-
-export default function() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isAdditionalNameList},
-    expose: {transform: value => value ?? []},
-  };
-}
diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js
index d5e7657e..e8f109d3 100644
--- a/src/data/composite/wiki-properties/index.js
+++ b/src/data/composite/wiki-properties/index.js
@@ -3,8 +3,6 @@
 // Entries here may depend on entries in #composite/control-flow,
 // #composite/data, and #composite/wiki-data.
 
-export {default as additionalFiles} from './additionalFiles.js';
-export {default as additionalNameList} from './additionalNameList.js';
 export {default as annotatedReferenceList} from './annotatedReferenceList.js';
 export {default as color} from './color.js';
 export {default as commentatorArtists} from './commentatorArtists.js';
@@ -23,7 +21,6 @@ export {default as name} from './name.js';
 export {default as referenceList} from './referenceList.js';
 export {default as referencedArtworkList} from './referencedArtworkList.js';
 export {default as reverseReferenceList} from './reverseReferenceList.js';
-export {default as seriesList} from './seriesList.js';
 export {default as simpleDate} from './simpleDate.js';
 export {default as simpleString} from './simpleString.js';
 export {default as singleReference} from './singleReference.js';
diff --git a/src/data/composite/wiki-properties/referencedArtworkList.js b/src/data/composite/wiki-properties/referencedArtworkList.js
index 9ba2e393..4f243493 100644
--- a/src/data/composite/wiki-properties/referencedArtworkList.js
+++ b/src/data/composite/wiki-properties/referencedArtworkList.js
@@ -1,6 +1,5 @@
 import {input, templateCompositeFrom} from '#composite';
 import find from '#find';
-import {isDate} from '#validators';
 
 import annotatedReferenceList from './annotatedReferenceList.js';
 
diff --git a/src/data/composite/wiki-properties/seriesList.js b/src/data/composite/wiki-properties/seriesList.js
deleted file mode 100644
index 2a101b45..00000000
--- a/src/data/composite/wiki-properties/seriesList.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import {input, templateCompositeFrom} from '#composite';
-import {isSeriesList, validateThing} from '#validators';
-
-import {exposeDependency} from '#composite/control-flow';
-import {withResolvedSeriesList} from '#composite/wiki-data';
-
-export default templateCompositeFrom({
-  annotation: `seriesList`,
-
-  compose: false,
-
-  inputs: {
-    group: input({
-      validate: validateThing({referenceType: 'group'}),
-    }),
-  },
-
-  steps: () => [
-    withResolvedSeriesList({
-      group: input('group'),
-
-      list: input.updateValue({
-        validate: isSeriesList,
-      }),
-    }),
-
-    exposeDependency({
-      dependency: '#resolvedSeriesList',
-    }),
-  ],
-});