« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/common-util/sort.js11
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js5
-rw-r--r--src/content/dependencies/generateArtistInfoPageChunkItem.js34
-rw-r--r--src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js4
-rw-r--r--src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js28
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunk.js92
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunkItem.js49
-rw-r--r--src/content/dependencies/generateTrackInfoPageOtherReleaseTooltip.js13
-rw-r--r--src/data/composite/things/contribution/inheritFromContributionPresets.js6
-rw-r--r--src/data/things/artist.js23
-rw-r--r--src/data/things/contribution.js8
-rw-r--r--src/data/things/track.js21
-rw-r--r--src/replacer.js9
-rw-r--r--src/static/css/site.css25
-rw-r--r--src/strings-default.yaml32
15 files changed, 226 insertions, 134 deletions
diff --git a/src/common-util/sort.js b/src/common-util/sort.js
index bbe4e551..b87ef500 100644
--- a/src/common-util/sort.js
+++ b/src/common-util/sort.js
@@ -45,11 +45,14 @@ export function normalizeName(s) {
     )
     .trim();
 
-  // Discard anything that isn't a letter, number, or space.
-  s = s.replace(/[^\p{Letter}\p{Number} ]/gu, '').trim();
+  // Discard anything that isn't a letter, number, space, or apostrophe.
+  s = s.replace(/[^\p{Letter}\p{Number} ']/gu, '').trim();
 
-  // Remove common English (only, for now) prefixes.
-  s = s.replace(/^(?:an?|the) /i, '');
+  // Remove common articles.
+  s = s.replace(/^(?:an?|the|le|la|l') /i, '');
+
+  // Discard apostrophes.
+  s = s.replace(/'/g, '').trim();
 
   return s;
 }
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
index e3ba5342..993ef706 100644
--- a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
@@ -21,9 +21,6 @@ export default {
         ? relation('linkTrack', contrib.thing.thing)
         : null),
 
-    otherArtistLinks:
-      relation('generateArtistInfoPageOtherArtistLinks', [contrib]),
-
     originDetails:
       relation('transformContent', contrib.thing.originDetails),
   }),
@@ -48,8 +45,6 @@ export default {
 
   generate: (data, relations, slots, {html, language}) =>
     relations.template.slots({
-      otherArtistLinks: relations.otherArtistLinks,
-
       annotation:
         language.encapsulate('artistPage.creditList.entry.artwork.accent', workingCapsule => {
           const workingOptions = {};
diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js
index 8117ca9a..92af420b 100644
--- a/src/content/dependencies/generateArtistInfoPageChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js
@@ -1,5 +1,3 @@
-import {empty} from '#sugar';
-
 export default {
   relations: (relation) => ({
     textWithTooltip:
@@ -17,8 +15,9 @@ export default {
       mutable: false,
     },
 
-    otherArtistLinks: {
-      validate: v => v.strictArrayOf(v.isHTML),
+    citation: {
+      type: 'html',
+      mutable: false,
     },
 
     rereleaseTooltip: {
@@ -54,11 +53,7 @@ export default {
                   text: language.$(entryCapsule, 'rerelease.term'),
                   tooltip: slots.rereleaseTooltip,
                 });
-
-              return language.$(workingCapsule, workingOptions);
-            }
-
-            if (!html.isBlank(slots.firstReleaseTooltip)) {
+            } else if (!html.isBlank(slots.firstReleaseTooltip)) {
               workingCapsule += '.firstRelease';
               workingOptions.firstRelease =
                 relations.textWithTooltip.slots({
@@ -66,29 +61,20 @@ export default {
                   text: language.$(entryCapsule, 'firstRelease.term'),
                   tooltip: slots.firstReleaseTooltip,
                 });
-
-              return language.$(workingCapsule, workingOptions);
-            }
-
-            let anyAccent = false;
-
-            if (!empty(slots.otherArtistLinks)) {
-              anyAccent = true;
-              workingCapsule += '.withArtists';
-              workingOptions.artists =
-                language.formatConjunctionList(slots.otherArtistLinks);
             }
 
             if (!html.isBlank(slots.annotation)) {
-              anyAccent = true;
               workingCapsule += '.withAnnotation';
               workingOptions.annotation = slots.annotation;
+            } else if (!html.isBlank(slots.citation)) {
+              workingCapsule += '.withCitation';
+              workingOptions.citation = slots.citation;
             }
 
-            if (anyAccent) {
-              return language.$(workingCapsule, workingOptions);
-            } else {
+            if (workingCapsule === entryCapsule) {
               return slots.content;
+            } else {
+              return language.$(workingCapsule, workingOptions);
             }
           }),
 
diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
index 08446a2e..572eb982 100644
--- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
@@ -231,7 +231,9 @@ export default {
                         type: itemTypes,
                       }).map(({item, link, annotation, type}) =>
                         item.slots({
-                          annotation:
+                          // The citation slot, instead of annotation, gives commentary
+                          // a specially custom look.
+                          citation:
                             annotation.slots({
                               mode: 'inline',
                               absorbPunctuationFollowingExternalLinks: false,
diff --git a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js
deleted file mode 100644
index afb61c33..00000000
--- a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import {unique} from '#sugar';
-
-export default {
-  query(contribs) {
-    const associatedContributionsByOtherArtists =
-      contribs
-        .flatMap(ownContrib =>
-          ownContrib.associatedContributions
-            .filter(associatedContrib =>
-              associatedContrib.artist !== ownContrib.artist));
-
-    const otherArtists =
-      unique(
-        associatedContributionsByOtherArtists
-          .map(contrib => contrib.artist));
-
-    return {otherArtists};
-  },
-
-  relations: (relation, query) => ({
-    artistLinks:
-      query.otherArtists
-        .map(artist => relation('linkArtist', artist)),
-  }),
-
-  generate: (relations) =>
-    relations.artistLinks,
-};
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunk.js b/src/content/dependencies/generateArtistInfoPageTracksChunk.js
index 7d00fdd6..50278271 100644
--- a/src/content/dependencies/generateArtistInfoPageTracksChunk.js
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunk.js
@@ -2,24 +2,70 @@ import {sortAlbumsTracksChronologically} from '#sort';
 import {empty, unique} from '#sugar';
 import {getTotalDuration} from '#wiki-data';
 
+function countTowardTotals(contribs) {
+  const track = contribs[0].thing;
+
+  if (track.isSecondaryRelease) {
+    const all =
+      Object.fromEntries(
+        unique(contribs.map(contrib => contrib.thingProperty))
+          .map(prop => [
+            prop,
+            track.mainReleaseTrack[prop].slice(),
+          ]));
+
+    contribs = contribs.flatMap(a => {
+      const array = all[a.thingProperty];
+      const index =
+        array.findIndex(b =>
+          b.artist === a.artist &&
+          b.annotation === a.annotation);
+
+      if (index === -1) return [];
+      return array.splice(index, 1);
+    }).filter(Boolean);
+  }
+
+  return contribs.some(contrib =>
+    contrib.countInContributionTotals ||
+    contrib.countInDurationTotals);
+}
+
 export default {
-  relations: (relation, artist, album, trackContribLists) => ({
+  query: (_artist, _album, trackContribLists) => ({
+    contribListsCountingTowardTotals:
+      trackContribLists
+        .filter(contribs => countTowardTotals(contribs)),
+
+    contribListsNotCountingTowardTotals:
+      trackContribLists
+        .filter(contribs => !countTowardTotals(contribs)),
+  }),
+
+  relations: (relation, query, artist, album, _trackContribLists) => ({
     template:
       relation('generateArtistInfoPageChunk'),
 
     albumLink:
       relation('linkAlbum', album),
 
-    // Intentional mapping here: each item may be associated with
-    // more than one contribution.
-    items:
-      trackContribLists.map(trackContribs =>
+    albumArtistCredit:
+      relation('generateArtistCredit', album.artistContribs, []),
+
+    itemsCountingTowardTotals:
+      query.contribListsCountingTowardTotals.map(trackContribs =>
+        relation('generateArtistInfoPageTracksChunkItem',
+          artist,
+          trackContribs)),
+
+    itemsNotCountingTowardTotals:
+      query.contribListsNotCountingTowardTotals.map(trackContribs =>
         relation('generateArtistInfoPageTracksChunkItem',
           artist,
           trackContribs)),
   }),
 
-  data(artist, album, trackContribLists) {
+  data(artist, _query, album, trackContribLists) {
     const data = {};
 
     const contribs =
@@ -71,10 +117,28 @@ export default {
     return data;
   },
 
-  generate: (data, relations, {html}) =>
+  generate: (data, relations, {html, language}) =>
     relations.template.slots({
       mode: 'album',
-      link: relations.albumLink,
+
+      link:
+        language.encapsulate('artistPage.creditList.album', workingCapsule => {
+          const creditCapsule = workingCapsule + '.credit';
+          const workingOptions = {album: relations.albumLink};
+
+          relations.albumArtistCredit.setSlots({
+            normalStringKey: creditCapsule + '.by',
+          });
+
+          if (!html.isBlank(relations.albumArtistCredit)) {
+            workingCapsule += '.withCredit';
+            workingOptions.credit =
+              html.tag('span', {class: 'by'},
+                relations.albumArtistCredit);
+          }
+
+          return language.$(workingCapsule, workingOptions);
+        }),
 
       dates: data.dates,
       duration: data.duration,
@@ -85,6 +149,16 @@ export default {
           data.numLinkingOtherReleases > 1 &&
             {class: 'offset-tooltips'},
 
-          relations.items),
+          [
+            relations.itemsCountingTowardTotals,
+
+            !empty(relations.itemsCountingTowardTotals) &&
+            !empty(relations.itemsNotCountingTowardTotals) &&
+              html.tag('li', {class: 'divider'},
+                html.tag('hr')),
+
+            relations.itemsNotCountingTowardTotals
+              .map(item => item.slot('showDuration', false)),
+          ]),
     }),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
index e976c57f..f53e0f81 100644
--- a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
@@ -10,9 +10,11 @@ export default {
     query.track =
       contribs[0].thing;
 
-    const creditedAsArtist =
+    const creditedAsNormalArtist =
       contribs
-        .some(contrib => contrib.thingProperty === 'artistContribs');
+        .some(contrib =>
+          contrib.thingProperty === 'artistContribs' &&
+          contrib.annotation !== 'featuring');
 
     const creditedAsContributor =
       contribs
@@ -20,7 +22,9 @@ export default {
 
     const annotatedContribs =
       contribs
-        .filter(contrib => contrib.annotation);
+        .filter(contrib =>
+          contrib.annotation &&
+          contrib.annotation !== 'featuring');
 
     const annotatedArtistContribs =
       annotatedContribs
@@ -39,7 +43,7 @@ export default {
     // Return seemingly only for "bass clarinet" when they're also
     // the one who composed and arranged Renewed Return!
     if (
-      creditedAsArtist &&
+      creditedAsNormalArtist &&
       creditedAsContributor &&
       empty(annotatedArtistContribs)
     ) {
@@ -84,15 +88,19 @@ export default {
     return query;
   },
 
-  relations: (relation, query, artist, contribs) => ({
+  relations: (relation, query, artist, _contribs) => ({
     template:
       relation('generateArtistInfoPageChunkItem'),
 
     trackLink:
       relation('linkTrack', query.track),
 
-    otherArtistLinks:
-      relation('generateArtistInfoPageOtherArtistLinks', contribs),
+    trackListItem:
+      relation('generateTrackListItem',
+        query.track,
+        (empty(query.track.album.artistContribs)
+          ? [artist.mockSimpleContribution]
+          : query.track.album.artistContribs)),
 
     rereleaseTooltip:
       (query.isLaterRelease
@@ -116,9 +124,15 @@ export default {
         : null),
   }),
 
-  generate: (data, relations, {html, language}) =>
+  slots: {
+    showDuration: {
+      type: 'boolean',
+      default: true,
+    },
+  },
+
+  generate: (data, relations, slots, {html, language}) =>
     relations.template.slots({
-      otherArtistLinks: relations.otherArtistLinks,
       rereleaseTooltip: relations.rereleaseTooltip,
       firstReleaseTooltip: relations.firstReleaseTooltip,
 
@@ -128,16 +142,13 @@ export default {
           : html.blank()),
 
       content:
-        language.encapsulate('artistPage.creditList.entry.track', workingCapsule => {
-          const workingOptions = {track: relations.trackLink};
-
-          if (data.duration) {
-            workingCapsule += '.withDuration';
-            workingOptions.duration =
-              language.formatDuration(data.duration);
-          }
-
-          return language.$(workingCapsule, workingOptions);
+        language.$('artistPage.creditList.entry.track', {
+          track:
+            html.inside(
+              relations.trackListItem.slots({
+                showArtists: 'auto',
+                showDuration: slots.showDuration,
+              })),
         }),
     }),
 };
diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleaseTooltip.js b/src/content/dependencies/generateTrackInfoPageOtherReleaseTooltip.js
index 92ab52ba..fcb2e2fa 100644
--- a/src/content/dependencies/generateTrackInfoPageOtherReleaseTooltip.js
+++ b/src/content/dependencies/generateTrackInfoPageOtherReleaseTooltip.js
@@ -43,11 +43,12 @@ export default {
           }),
 
           data.otherDate && data.currentDate &&
-            language.formatRelativeDate(data.otherDate, data.currentDate, {
-              considerRoundingDays: true,
-              approximate: true,
-              absolute: false,
-            }),
+            html.tag('span', {class: 'when'},
+              language.formatRelativeDate(data.otherDate, data.currentDate, {
+                considerRoundingDays: true,
+                approximate: true,
+                absolute: false,
+              })),
         ],
       })),
-};
\ No newline at end of file
+};
diff --git a/src/data/composite/things/contribution/inheritFromContributionPresets.js b/src/data/composite/things/contribution/inheritFromContributionPresets.js
index 17387404..1cefae1b 100644
--- a/src/data/composite/things/contribution/inheritFromContributionPresets.js
+++ b/src/data/composite/things/contribution/inheritFromContributionPresets.js
@@ -41,10 +41,8 @@ export default templateCompositeFrom({
       compute: (continuation, {
         ['#values']: values,
         ['#index']: index,
-      }) => continuation({
-        ['#value']:
-          values[index],
-      }),
+      }) =>
+        continuation.exit(values[index]),
     },
   ],
 });
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index a2ed0b74..01eb2172 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -43,7 +43,7 @@ export class Artist extends Thing {
     'avatarArtwork', // from inline fields
   ];
 
-  static [Thing.getPropertyDescriptors] = () => ({
+  static [Thing.getPropertyDescriptors] = ({Contribution}) => ({
     // Update & expose
 
     name: name(V('Unnamed Artist')),
@@ -78,6 +78,27 @@ export class Artist extends Thing {
 
     isArtist: exposeConstant(V(true)),
 
+    mockSimpleContribution: {
+      flags: {expose: true},
+      expose: {
+        dependencies: ['directory', '_find'],
+        compute: ({directory, _find: find}) =>
+          Object.assign(new Contribution, {
+            artist: 'artist:' + directory,
+
+            // These nulls have no effect, they're only included
+            // here for clarity.
+            date: null,
+            thing: null,
+            annotation: null,
+            artistProperty: null,
+            thingProperty: null,
+
+            find,
+          }),
+      },
+    },
+
     trackArtistContributions: reverseReferenceList({
       reverse: soupyReverse.input('trackArtistContributionsBy'),
     }),
diff --git a/src/data/things/contribution.js b/src/data/things/contribution.js
index 393a60b4..778bc566 100644
--- a/src/data/things/contribution.js
+++ b/src/data/things/contribution.js
@@ -62,12 +62,12 @@ export class Contribution extends Thing {
     },
 
     countInContributionTotals: [
-      inheritFromContributionPresets(),
-
       exposeUpdateValueOrContinue({
         validate: input.value(isBoolean),
       }),
 
+      inheritFromContributionPresets(),
+
       {
         dependencies: ['thing', input.myself()],
         compute: (continuation, {
@@ -85,12 +85,12 @@ export class Contribution extends Thing {
     ],
 
     countInDurationTotals: [
-      inheritFromContributionPresets(),
-
       exposeUpdateValueOrContinue({
         validate: input.value(isBoolean),
       }),
 
+      inheritFromContributionPresets(),
+
       withPropertyFromObject('thing', V('duration')),
       exitWithoutDependency('#thing.duration', {
         value: input.value(false),
diff --git a/src/data/things/track.js b/src/data/things/track.js
index f77cfa41..3c4b5409 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -289,12 +289,20 @@ export class Track extends Thing {
     ],
 
     contributorContribs: [
-      inheritContributionListFromMainRelease(),
-
-      contributionList({
+      withResolvedContribs({
+        from: input.updateValue({validate: isContributionList}),
         date: 'date',
-        artistProperty: input.value('trackContributorContributions'),
+        thingProperty: input.thisProperty(),
+        artistProperty: input.value('trackArtistContributions'),
+      }).outputs({
+        '#resolvedContribs': '#contributorContribs',
       }),
+
+      exposeDependencyOrContinue('#contributorContribs', V('empty')),
+
+      inheritContributionListFromMainRelease(),
+
+      exposeConstant(V([])),
     ],
 
     // > Update & expose - General configuration
@@ -1047,11 +1055,6 @@ export class Track extends Thing {
         'Sampled Tracks',
       ]},
 
-      {message: `Secondary releases inherit contributors from the main one`, fields: [
-        'Main Release',
-        'Contributors',
-      ]},
-
       {
         message: ({'Has Cover Art': hasCoverArt}) =>
           (hasCoverArt
diff --git a/src/replacer.js b/src/replacer.js
index 8a929444..9be14755 100644
--- a/src/replacer.js
+++ b/src/replacer.js
@@ -934,7 +934,7 @@ export function postprocessExternalLinks(inputNodes) {
 
     let parseFrom = 0;
     for (const match of matchInlineLinks(node.data)) {
-      const {href, index, length} = match;
+      let {href, index, length} = match;
 
       textNode.data += node.data.slice(parseFrom, index);
 
@@ -950,6 +950,13 @@ export function postprocessExternalLinks(inputNodes) {
         };
       }
 
+      try {
+        const url = new URL(href);
+        if (url.pathname === '/' && !url.search && !url.hash) {
+          href = href.replace(/\/$/, '');
+        }
+      } catch {}
+
       outputNodes.push({
         i: node.i + index,
         iEnd: node.i + index + length,
diff --git a/src/static/css/site.css b/src/static/css/site.css
index 9cfa4417..1e3a781a 100644
--- a/src/static/css/site.css
+++ b/src/static/css/site.css
@@ -1518,6 +1518,14 @@ label > input[type=checkbox]:not(:checked) + span {
   font-size: 0.9em;
 }
 
+.other-release-tooltip .tooltip-content .when {
+  /* technically just putting this in a <span> was enough
+   * to keep it from wrapping all tight-like, for some
+   * reason, but im not taking any chances...
+   */
+  white-space: nowrap;
+}
+
 .rerelease-tooltip .not-credited-on-first-release {
   opacity: 0.9;
 }
@@ -2088,6 +2096,19 @@ h1 {
   white-space: nowrap;
 }
 
+#content li.divider {
+  list-style-type: none;
+  max-width: 220px;
+  margin-top: 0.5em;
+  margin-bottom: 0.5em;
+}
+
+#content li.divider hr {
+  color: #888;
+  border: none;
+  border-bottom: 1px solid;
+}
+
 #content details {
   margin-top: 0.25em;
   margin-bottom: 0.25em;
@@ -2263,11 +2284,11 @@ ul.quick-info li:not(:last-child)::after {
   margin: 0 6px;
 }
 
-li .by {
+dt .by, li .by {
   font-style: oblique;
 }
 
-li .by a {
+dt .by a, li .by a {
   display: inline-block;
 }
 
diff --git a/src/strings-default.yaml b/src/strings-default.yaml
index 909ae043..b79dd67f 100644
--- a/src/strings-default.yaml
+++ b/src/strings-default.yaml
@@ -1285,6 +1285,11 @@ artistPage:
 
     album:
       _: "{ALBUM}"
+
+      withCredit: "{ALBUM} {CREDIT}"
+      credit:
+        by: "by {ARTISTS}"
+
       withDate: "{ALBUM} ({DATE})"
       withDuration: "{ALBUM} ({DURATION})"
       withDate.withDuration: "{ALBUM} ({DATE}; {DURATION})"
@@ -1313,15 +1318,13 @@ artistPage:
       #   thing may be described with a word or two, and that's shown
       #   in the list.
 
-      withAnnotation: "{ENTRY} ({ANNOTATION})"
+      withAnnotation: "{ENTRY} — {ANNOTATION}"
 
-      # withArtists:
-      #   This lists co-artists or co-contributors, depending on how
-      #   the artist themselves was credited.
+      # withCitation:
+      #   For commentary entries and the like. These are usually longer
+      #   than typical annotations.
 
-      withArtists: "{ENTRY} (with {ARTISTS})"
-
-      withArtists.withAnnotation: "{ENTRY} ({ANNOTATION}; with {ARTISTS})"
+      withCitation: "{ENTRY} ({CITATION})"
 
       # rerelease:
       #   Tracks which aren't the original release don't display co-
@@ -1330,28 +1333,24 @@ artistPage:
 
       rerelease:
         _: "{ENTRY} ({RERELEASE})"
+        withAnnotation: "{ENTRY} ({RERELEASE}) — {ANNOTATION}"
         term: "rerelease"
 
         firstRelease: >-
           First released on {ALBUM}
 
         notCreditedOnFirstRelease: >-
-          Note that {ARTIST} is not credited on this track's first release.
+          {ARTIST} isn't credited on this track's first release.
 
       firstRelease:
         _: "{ENTRY} ({FIRST_RELEASE})"
+        withAnnotation: "{ENTRY} ({FIRST_RELEASE}) — {ANNOTATION}"
         term: "first release"
 
         rerelease: >-
           Also released on {ALBUM}
 
-      # track:
-      #   The string without duration is used in both the artist's
-      #   track credits list as well as their commentary list.
-
-      track:
-        _: "{TRACK}"
-        withDuration: "({DURATION}) {TRACK}"
+      track: "{TRACK}"
 
       # album:
       #   The artist info page doesn't display if the artist is
@@ -1366,8 +1365,7 @@ artistPage:
         bannerArt: "(banner art)"
         commentary: "(album commentary)"
 
-      flash:
-        _: "{FLASH}"
+      flash: "{FLASH}"
 
       artwork.accent:
         withLabel: >-