« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js141
-rw-r--r--src/content/dependencies/generateContentEntry.js8
-rw-r--r--src/content/dependencies/generateContentEntryDate.js2
-rw-r--r--src/content/dependencies/generateLyricsEntry.js4
-rw-r--r--src/content/dependencies/generateTrackInfoPageOtherReleaseTooltip.js16
-rw-r--r--src/data/checks.js7
-rw-r--r--src/data/composite.js32
-rw-r--r--src/data/composite/things/content/withExpressedOrImplicitArtistReferences.js29
-rw-r--r--src/data/things/content/ContentEntry.js53
-rw-r--r--src/search-select.js3
-rw-r--r--src/static/css/responsive.css2
-rw-r--r--src/static/css/search.css63
-rw-r--r--src/static/js/client/sidebar-search.js24
-rw-r--r--src/strings-default.yaml5
14 files changed, 276 insertions, 113 deletions
diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
index 572eb982..6b603375 100644
--- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
@@ -30,6 +30,10 @@ export default {
         flashAct,
         flash,
 
+        quoted:
+         !entry.headingArtists.includes(artist) &&
+          entry.quotedArtists.includes(artist),
+
         annotation: entry.annotation,
         annotationParts: entry.annotationParts,
       },
@@ -191,6 +195,11 @@ export default {
       query.chunks
         .map(({chunk}) => chunk
           .map(({itemType}) => itemType)),
+
+    itemQuoted:
+      query.chunks
+        .map(({chunk}) => chunk
+          .map(({quoted}) => quoted)),
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -206,6 +215,7 @@ export default {
         itemLinks: relations.itemLinks,
         itemAnnotations: relations.itemAnnotations,
         itemTypes: data.itemTypes,
+        itemQuoted: data.itemQuoted,
       }).map(({
           chunk,
           chunkLink,
@@ -215,64 +225,77 @@ export default {
           itemLinks,
           itemAnnotations,
           itemTypes,
+          itemQuoted,
         }) =>
-          language.encapsulate('artistPage.creditList.entry', capsule =>
-            (chunkType === 'album'
-              ? chunk.slots({
-                  mode: 'album',
-                  link: chunkLink,
-
-                  list:
-                    html.tag('ul',
-                      stitchArrays({
-                        item: items,
-                        link: itemLinks,
-                        annotation: itemAnnotations,
-                        type: itemTypes,
-                      }).map(({item, link, annotation, type}) =>
-                        item.slots({
-                          // The citation slot, instead of annotation, gives commentary
-                          // a specially custom look.
-                          citation:
-                            annotation.slots({
-                              mode: 'inline',
-                              absorbPunctuationFollowingExternalLinks: false,
-                            }),
-
-                          content:
-                            (type === 'album'
-                              ? html.tag('i',
-                                  language.$(capsule, 'album.commentary'))
-                              : language.$(capsule, 'track', {track: link})),
-                        }))),
-                })
-
-             : chunkType === 'flash-act'
-              ? chunk.slots({
-                  mode: 'flash',
-                  link: chunkLink,
-
-                  list:
-                    html.tag('ul',
-                      stitchArrays({
-                        item: items,
-                        link: itemLinks,
-                        annotation: itemAnnotations,
-                      }).map(({item, link, annotation}) =>
-                        item.slots({
-                          annotation:
-                            (annotation
-                              ? annotation.slots({
-                                  mode: 'inline',
-                                  absorbPunctuationFollowingExternalLinks: false,
-                                })
-                              : null),
-
-                          content:
-                            language.$(capsule, 'flash', {
-                              flash: link,
-                            }),
-                        }))),
-                })
-              : null)))),
+          language.encapsulate('artistPage.creditList.entry', capsule => {
+            // The citation slot, instead of annotation, gives commentary
+            // a specially custom look.
+            const citations =
+              stitchArrays({annotation: itemAnnotations, quoted: itemQuoted})
+                .map(({annotation, quoted}) =>
+                  language.encapsulate(capsule, workingCapsule => {
+                    const workingOptions = {};
+
+                    let any = false;
+
+                    annotation.setSlots({
+                      mode: 'inline',
+                      absorbPunctuationFollowingExternalLinks: false,
+                    });
+
+                    if (!html.isBlank(annotation)) {
+                      workingCapsule += '.citation';
+                      workingOptions.citation = annotation;
+                      any = true;
+                    }
+
+                    if (quoted) {
+                      workingCapsule += '.quoted';
+                      any = true;
+                    }
+
+                    if (any) {
+                      return language.$(workingCapsule, workingOptions);
+                    } else {
+                      return html.blank();
+                    }
+                  }));
+
+            let contents;
+
+            if (chunkType === 'album') {
+              chunk.setSlot('mode', 'album');
+              contents =
+                stitchArrays({link: itemLinks, type: itemTypes})
+                  .map(({link, type}) =>
+                    (type === 'album'
+                      ? html.tag('i',
+                          language.$(capsule, 'album.commentary'))
+                      : language.$(capsule, 'track', {track: link})));
+
+            } else if (chunkType === 'flash-act') {
+              chunk.setSlot('mode', 'flash');
+              contents =
+                itemLinks.map(link =>
+                  language.$(capsule, 'flash', {flash: link}));
+
+            } else {
+              throw new Error(`Gyeep!!`);
+            }
+
+            chunk.setSlots({
+              link: chunkLink,
+
+              list:
+                html.tag('ul',
+                  stitchArrays({
+                    item: items,
+                    citation: citations,
+                    content: contents,
+                  }).map(({item, citation, content}) =>
+                      item.slots({citation, content}))),
+            });
+
+            return chunk;
+          }))),
 };
diff --git a/src/content/dependencies/generateContentEntry.js b/src/content/dependencies/generateContentEntry.js
index c77f744a..40c637a3 100644
--- a/src/content/dependencies/generateContentEntry.js
+++ b/src/content/dependencies/generateContentEntry.js
@@ -3,14 +3,14 @@ import {empty} from '#sugar';
 export default {
   relations: (relation, entry) => ({
     artistLinks:
-      (!empty(entry.artists) && !entry.artistText
-        ? entry.artists
+      (!empty(entry.headingArtists) && !entry.headingArtistText
+        ? entry.headingArtists
             .map(artist => relation('linkArtist', artist))
         : null),
 
     artistsContent:
-      (entry.artistText
-        ? relation('transformContent', entry.artistText)
+      (entry.headingArtistText
+        ? relation('transformContent', entry.headingArtistText)
         : null),
 
     annotationContent:
diff --git a/src/content/dependencies/generateContentEntryDate.js b/src/content/dependencies/generateContentEntryDate.js
index 845cb5ed..5255c7ea 100644
--- a/src/content/dependencies/generateContentEntryDate.js
+++ b/src/content/dependencies/generateContentEntryDate.js
@@ -35,7 +35,7 @@ export default {
 
      : entry.thing.isTrack &&
        entry.thing.date === entry.thing.album.date &&
-       entry.thing.style === 'single'
+       entry.thing.album.style === 'single'
         ? 'single'
 
      : entry.thing.isTrack &&
diff --git a/src/content/dependencies/generateLyricsEntry.js b/src/content/dependencies/generateLyricsEntry.js
index 15f84b27..0ecf319f 100644
--- a/src/content/dependencies/generateLyricsEntry.js
+++ b/src/content/dependencies/generateLyricsEntry.js
@@ -4,10 +4,10 @@ export default {
       relation('transformContent', entry.body),
 
     artistText:
-      relation('transformContent', entry.artistText),
+      relation('transformContent', entry.headingArtistText),
 
     artistLinks:
-      entry.artists
+      entry.headingArtists
         .filter(artist => artist.name !== 'HSMusic Wiki') // smh
         .map(artist => relation('linkArtist', artist)),
 
diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleaseTooltip.js b/src/content/dependencies/generateTrackInfoPageOtherReleaseTooltip.js
index fcb2e2fa..4c6bda1b 100644
--- a/src/content/dependencies/generateTrackInfoPageOtherReleaseTooltip.js
+++ b/src/content/dependencies/generateTrackInfoPageOtherReleaseTooltip.js
@@ -20,6 +20,9 @@ export default {
       (compareKebabCase(otherTrack.name, currentTrack.name)
         ? null
         : otherTrack.name),
+
+    onSingle:
+      otherTrack.album.style === 'single',
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -36,10 +39,17 @@ export default {
         ],
 
         content: [
-          language.$(capsule, 'differentName', {
-            [language.onlyIfOptions]: ['name'],
+          language.encapsulate(capsule, 'differentName', workingCapsule => {
+            const workingOptions = {
+              [language.onlyIfOptions]: ['name'],
+              name: data.differentName,
+            };
+
+            if (data.onSingle) {
+              workingCapsule += '.onSingle';
+            }
 
-            name: data.differentName,
+            return language.$(workingCapsule, workingOptions);
           }),
 
           data.otherDate && data.currentDate &&
diff --git a/src/data/checks.js b/src/data/checks.js
index 0a0e7f52..01b5cf9e 100644
--- a/src/data/checks.js
+++ b/src/data/checks.js
@@ -352,6 +352,9 @@ export function filterReferenceErrors(wikiData, {
 
             switch (findFnKey) {
               case '_content':
+                // note: quoted artists ("Quoted Artists" field)`) currently
+                // not handled here at all. limitation of current code! oops!
+
                 if (value) {
                   value =
                     value.map(entry =>
@@ -721,13 +724,13 @@ export function reportContentTextErrors(wikiData, {
 
   const commentaryShape = {
     body: 'commentary body',
-    artistText: 'commentary artist text',
+    headingArtistText: 'commentary artist text',
     annotation: 'commentary annotation',
   };
 
   const lyricsShape = {
     body: 'lyrics body',
-    artistText: 'lyrics artist text',
+    headingArtistText: 'lyrics artist text',
     annotation: 'lyrics annotation',
   };
 
diff --git a/src/data/composite.js b/src/data/composite.js
index 8ac906c7..3b462ef5 100644
--- a/src/data/composite.js
+++ b/src/data/composite.js
@@ -10,17 +10,27 @@ import {TupleMap} from '#wiki-data';
 
 const globalCompositeCache = {};
 
-const _valueIntoToken = shape =>
-  (value = null) =>
-    (value === null
-      ? Symbol.for(`hsmusic.composite.${shape}`)
-   : typeof value === 'string'
-      ? Symbol.for(`hsmusic.composite.${shape}:${value}`)
-      : {
-          symbol: Symbol.for(`hsmusic.composite.${shape.split('.')[0]}`),
-          shape,
-          value,
-        });
+const _valueIntoToken = shape => (value = null) => {
+  if (value === null) {
+    return Symbol.for(`hsmusic.composite.${shape}`);
+  }
+
+  if (typeof value === 'string') {
+    return Symbol.for(`hsmusic.composite.${shape}:${value}`);
+  }
+
+  if (typeof value === 'object') {
+    if (Object.values(value).some(isInputToken)) {
+      throw new TypeError(`Don't nest input tokens inside ${shape}()`);
+    }
+  }
+
+  return {
+    symbol: Symbol.for(`hsmusic.composite.${shape.split('.')[0]}`),
+    shape,
+    value,
+  };
+};
 
 export const input = _valueIntoToken('input');
 input.symbol = Symbol.for('hsmusic.composite.input');
diff --git a/src/data/composite/things/content/withExpressedOrImplicitArtistReferences.js b/src/data/composite/things/content/withExpressedOrImplicitArtistReferences.js
index 69da8c75..a6200ee8 100644
--- a/src/data/composite/things/content/withExpressedOrImplicitArtistReferences.js
+++ b/src/data/composite/things/content/withExpressedOrImplicitArtistReferences.js
@@ -8,16 +8,19 @@ export default templateCompositeFrom({
   annotation: `withExpressedOrImplicitArtistReferences`,
 
   inputs: {
-    from: input({type: 'array', acceptsNull: true}),
+    fromExpressed: input({type: 'array', acceptsNull: true}),
+    fromContent: input({type: 'string', acceptsNull: true}),
+
+    filterArtistTags: input({type: 'function', defaultValue: () => true}),
   },
 
   outputs: ['#artistReferences'],
 
   steps: () => [
     {
-      dependencies: [input('from')],
+      dependencies: [input('fromExpressed')],
       compute: (continuation, {
-        [input('from')]: expressedArtistReferences,
+        [input('fromExpressed')]: expressedArtistReferences,
       }) =>
         (expressedArtistReferences
           ? continuation.raiseOutput({'#artistReferences': expressedArtistReferences})
@@ -25,12 +28,12 @@ export default templateCompositeFrom({
     },
 
     raiseOutputWithoutDependency({
-      dependency: 'artistText',
-      output: input.value({'#artistReferences': null}),
+      dependency: input('fromContent'),
+      output: input.value({'#artistReferences': []}),
     }),
 
     withContentNodes({
-      from: 'artistText',
+      from: input('fromContent'),
     }),
 
     withMappedList({
@@ -42,13 +45,19 @@ export default templateCompositeFrom({
       '#mappedList': '#artistTagFilter',
     }),
 
-    withFilteredList({
-      list: '#contentNodes',
-      filter: '#artistTagFilter',
+    withFilteredList('#contentNodes', '#artistTagFilter')
+      .outputs({'#filteredList': '#artistTags'}),
+
+    withMappedList({
+      list: '#artistTags',
+      map: input('filterArtistTags'),
     }).outputs({
-      '#filteredList': '#artistTags',
+      '#mappedList': '#customFilter',
     }),
 
+    withFilteredList({list: '#artistTags', filter: '#customFilter'})
+      .outputs({'#filteredList': '#artistTags'}),
+
     withMappedList({
       list: '#artistTags',
       map: input.value(node =>
diff --git a/src/data/things/content/ContentEntry.js b/src/data/things/content/ContentEntry.js
index 04df303f..47f86622 100644
--- a/src/data/things/content/ContentEntry.js
+++ b/src/data/things/content/ContentEntry.js
@@ -1,5 +1,5 @@
 import {input, V} from '#composite';
-import {transposeArrays} from '#sugar';
+import {transposeArrays, unique} from '#sugar';
 import Thing from '#thing';
 import {is, isDate, validateReferenceList} from '#validators';
 import {parseDate} from '#yaml';
@@ -33,14 +33,43 @@ export class ContentEntry extends Thing {
 
     thing: thing(),
 
-    artists: [
+    headingArtists: [
       withExpressedOrImplicitArtistReferences({
-        from: input.updateValue({
+        fromExpressed: input.updateValue({
           validate: validateReferenceList('artist'),
         }),
+
+        fromContent: 'headingArtistText',
+      }),
+
+      withResolvedReferenceList({
+        list: '#artistReferences',
+        find: soupyFind.input('artist'),
       }),
 
-      exitWithoutDependency('#artistReferences', V([])),
+      exposeDependency('#resolvedReferenceList'),
+    ],
+
+    quotedArtists: [
+      exitWithoutDependency('body', V([])),
+
+      {
+        dependencies: ['body'],
+        compute: (continuation, {body}) => continuation({
+          ['#filterArtistTags']: node =>
+            /(\n|^)> <i>$/.test(body.slice(0, node.i)) &&
+            /^:<\/i>/.test(body.slice(node.iEnd)),
+        }),
+      },
+
+      withExpressedOrImplicitArtistReferences({
+        fromExpressed: input.updateValue({
+          validate: validateReferenceList('artist'),
+        }),
+
+        fromContent: 'body',
+        filterArtistTags: '#filterArtistTags',
+      }),
 
       withResolvedReferenceList({
         list: '#artistReferences',
@@ -50,7 +79,7 @@ export class ContentEntry extends Thing {
       exposeDependency('#resolvedReferenceList'),
     ],
 
-    artistText: contentString(),
+    headingArtistText: contentString(),
 
     annotation: contentString(),
 
@@ -119,6 +148,14 @@ export class ContentEntry extends Thing {
 
     isContentEntry: exposeConstant(V(true)),
 
+    artists: [
+      {
+        dependencies: ['headingArtists', 'quotedArtists'],
+        compute: ({headingArtists, quotedArtists}) =>
+          unique([...headingArtists, ...quotedArtists]),
+      },
+    ],
+
     annotationParts: [
       withAnnotationPartNodeLists(),
 
@@ -230,8 +267,10 @@ export class ContentEntry extends Thing {
 
   static [Thing.yamlDocumentSpec] = {
     fields: {
-      'Artists': {property: 'artists'},
-      'Artist Text': {property: 'artistText'},
+      'Artists': {property: 'headingArtists'},
+      'Artist Text': {property: 'headingArtistText'},
+
+      'Quoted Artists': {property: 'quotedArtists'},
 
       'Annotation': {property: 'annotation'},
 
diff --git a/src/search-select.js b/src/search-select.js
index 3133c642..dc1508be 100644
--- a/src/search-select.js
+++ b/src/search-select.js
@@ -158,7 +158,8 @@ function genericSelect(wikiData) {
   return [
     sortByGroupRank(wikiData.albumData.slice()),
 
-    wikiData.artTagData,
+    wikiData.artTagData
+      .filter(artTag => !artTag.isContentWarning),
 
     wikiData.artistData
       .filter(artist => !artist.isAlias),
diff --git a/src/static/css/responsive.css b/src/static/css/responsive.css
index 86cd7eb6..38e4188a 100644
--- a/src/static/css/responsive.css
+++ b/src/static/css/responsive.css
@@ -106,7 +106,7 @@
   }
 
   .wiki-search-sidebar-box {
-    max-height: max(245px, 60vh, calc(100vh - 205px));
+    --keep-viewport-visible: 205px;
   }
 
   /* End duplicated for "sidebars in content column" */
diff --git a/src/static/css/search.css b/src/static/css/search.css
index f421803b..3c56eed6 100644
--- a/src/static/css/search.css
+++ b/src/static/css/search.css
@@ -5,11 +5,17 @@
     padding: 1px 0 0 0;
 
     z-index: 100;
-    max-height: calc(100vh - 20px);
+
+    --keep-viewport-visible: 125px;
+    max-height: max(245px, 60vh, calc(100vh - var(--keep-viewport-visible)));
 
     display: flex;
     flex-direction: column;
   }
+
+  #banner.short ~ * .wiki-search-sidebar-box {
+    --keep-viewport-visible: 180px;
+  }
 }
 
 @layer material {
@@ -32,17 +38,20 @@
 
 /* Interactions with other sidebar boxes */
 
-@layer interaction {
-  /* This is to say, any sidebar that's *not*
-   * the first sidebar after the search box.
+@layer layout {
+  /* This is to say, any sidebar that's *not* the first sidebar
+   * after the search box, effectively squishing the rest of the
+   * boxes a bit tighter together.
    */
   .wiki-search-sidebar-box.showing-results + .sidebar ~ .sidebar {
-    margin-top: 5px;
+    margin-top: 8px;
   }
+}
 
+@layer interaction {
   .wiki-search-sidebar-box.showing-results ~ .sidebar:not(:hover) {
     opacity: 0.8;
-    filter: brightness(0.7);
+    filter: brightness(0.85);
   }
 }
 
@@ -191,6 +200,8 @@
 @layer layout {
   .wiki-search-context-container {
     padding: 2px 12px 4px;
+    padding-left: calc(12px + 1.2ch);
+    text-indent: -1.2ch;
   }
 }
 
@@ -322,11 +333,51 @@
 
 @layer layout {
   .wiki-search-results-container {
+    position: relative;
     margin-bottom: 0;
     padding: 2px;
   }
 }
 
+@layer interaction {
+  .wiki-search-results-container::before,
+  .wiki-search-results-container::after {
+    content: "";
+    display: block;
+    position: sticky;
+    pointer-events: none;
+    z-index: 1;
+  }
+
+  .wiki-search-result:hover {
+    z-index: 2;
+  }
+
+  /* Shadow along top edge */
+
+  .wiki-search-results-container > :first-child {
+    margin-top: -4px;
+  }
+
+  .wiki-search-results-container::before {
+    height: 8px; top: -2px;
+    background: linear-gradient(to bottom, black, black 50%, transparent);
+    opacity: 0.4;
+  }
+
+  /* Shadow along bottom edge */
+
+  .wiki-search-results-container > :last-child {
+    margin-bottom: -10px;
+  }
+
+  .wiki-search-results-container::after {
+    height: 16px; bottom: -2px;
+    background: linear-gradient(to top, black, black 30%, transparent);
+    opacity: 0.4;
+  }
+}
+
 /* Basic result styling, including interactions */
 
 @layer layout {
diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js
index c39c38bc..7b01cb00 100644
--- a/src/static/js/client/sidebar-search.js
+++ b/src/static/js/client/sidebar-search.js
@@ -106,6 +106,8 @@ export const info = {
     recallingRecentSearch: null,
     recallingRecentSearchFromMouse: null,
 
+    justPerformedActiveQuery: false,
+
     currentValue: null,
 
     workerStatus: null,
@@ -585,7 +587,6 @@ export function addPageListeners() {
     clearSidebarSearch();
     clearSidebarFilter();
     possiblyHideSearchSidebarColumn();
-    restoreSidebarSearchColumn();
   });
 
   forEachFilter((type, filterLink) => {
@@ -731,6 +732,7 @@ async function activateSidebarSearch(query) {
     return;
   }
 
+  state.justPerformedActiveQuery = true;
   state.searchStage = 'complete';
   updateSidebarSearchStatus();
 
@@ -796,6 +798,7 @@ function clearSidebarSearch() {
   info.searchInput.value = '';
 
   state.searchStage = null;
+  state.justPerformedActiveQuery = false;
 
   clearActiveQuery();
 
@@ -1383,6 +1386,8 @@ function hideSidebarSearchResults() {
 
   cssProp(info.endSearchRule, 'display', 'none');
   cssProp(info.endSearchLine, 'display', 'none');
+
+  restoreSidebarSearchColumn();
 }
 
 function focusFirstSidebarSearchResult() {
@@ -1466,7 +1471,7 @@ function possiblyHideSearchSidebarColumn() {
 // This should be called after results are shown, since it checks the
 // elements added to understand the current search state.
 function tidySidebarSearchColumn() {
-  const {state} = info;
+  const {session, state} = info;
 
   // Don't *re-tidy* the sidebar if we've already tidied it to display
   // some results. This flag will get cleared if the search is dismissed
@@ -1475,17 +1480,24 @@ function tidySidebarSearchColumn() {
     return;
   }
 
-  const here = location.href.replace(/\/$/, '');
+  const hrefHere = location.href.replace(/\/$/, '');
   const currentPageIsResult =
     Array.from(info.results.querySelectorAll('a'))
       .some(link => {
-        const there = link.href.replace(/\/$/, '');
-        return here === there;
+        const hrefThere = link.href.replace(/\/$/, '');
+        return hrefHere === hrefThere;
       });
 
+  const currentPageIsContext =
+    location.pathname === session.activeQueryContextPagePathname;
+
   // Don't tidy the sidebar if you've navigated to some other page than
   // what's in the current result list.
-  if (!currentPageIsResult) {
+  if (
+    !state.justPerformedActiveQuery &&
+    !currentPageIsResult &&
+    !currentPageIsContext
+  ) {
     return;
   }
 
diff --git a/src/strings-default.yaml b/src/strings-default.yaml
index 54741239..ed8f236f 100644
--- a/src/strings-default.yaml
+++ b/src/strings-default.yaml
@@ -310,6 +310,7 @@ releaseInfo:
 
     tooltip:
       differentName: "as {NAME}"
+      differentName.onSingle: "{NAME}"
 
   tracksReferenced:
     _: "Tracks that {TRACK} references:"
@@ -1395,6 +1396,10 @@ artistPage:
 
       withCitation: "{ENTRY} ({CITATION})"
 
+      citation: "{CITATION}"
+      citation.quoted: "quoted: {CITATION}"
+      quoted: "quoted"
+
       # rerelease:
       #   Tracks which aren't the original release don't display co-
       #   artists or contributors, and get dimmed a little compared