« 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/content/dependencies/generatePageLayout.js53
-rw-r--r--src/content/dependencies/generatePageTitleText.js67
-rw-r--r--src/content/dependencies/generateSearchSidebarBox.js5
-rw-r--r--src/data/things/track.js2
-rw-r--r--src/data/yaml.js52
-rw-r--r--src/static/css/site.css6
-rw-r--r--src/static/js/client/sidebar-search.js74
-rw-r--r--src/strings-default.yaml2
8 files changed, 200 insertions, 61 deletions
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index 23d5932d..fec3bd78 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -1,5 +1,3 @@
-import striptags from 'striptags';
-
 import {openAggregate} from '#aggregate';
 import {atOffset, empty, repeat} from '#sugar';
 
@@ -27,6 +25,9 @@ export default {
     relations.stickyHeadingContainer =
       relation('generateStickyHeadingContainer');
 
+    relations.titleText =
+      relation('generatePageTitleText');
+
     relations.sidebar =
       relation('generatePageSidebar');
 
@@ -629,6 +630,12 @@ export default {
       footerHTML,
     ];
 
+    relations.titleText.setSlots({
+      title: slots.title,
+      showWikiNameInTitle: slots.showWikiNameInTitle,
+      subtitle: slots.subtitle,
+    });
+
     const pageHTML = html.tags([
       `<!DOCTYPE html>`,
       html.tag('html',
@@ -653,44 +660,12 @@ export default {
 
           html.tag('head', [
             html.tag('title',
-              language.encapsulate('misc.pageTitle', workingCapsule => {
-                const workingOptions = {};
-
-                // Slightly jank: The output of striptags is, of course, a string,
-                // and as far as language.formatString() is concerned, that means
-                // it needs to be sanitized - including turning ampersands into
-                // &amp;'s. But the title is already HTML that has implicitly been
-                // sanitized, however it got here, and includes HTML entities that
-                // are properly escaped. Those need to get included as they are,
-                // so we wrap the title in a tag and pass it off as good to go.
-                workingOptions.title =
-                  html.tags([
-                    striptags(slots.title.toString()),
-                  ]);
-
-                if (!html.isBlank(slots.subtitle)) {
-                  // Same shenanigans here, as far as wrapping striptags goes.
-                  workingCapsule += '.withSubtitle';
-                  workingOptions.subtitle =
-                    html.tags([
-                      striptags(slots.subtitle.toString()),
-                    ]);
-                }
+              {'data-without-wiki-name':
+                relations.titleText.clone()
+                  .slot('showWikiNameInTitle', false)
+                  .toString()},
 
-                const showWikiName =
-                  (slots.showWikiNameInTitle === true
-                    ? true
-                 : slots.showWikiNameInTitle === 'auto'
-                    ? html.isBlank(slots.subtitle)
-                    : false);
-
-                if (showWikiName) {
-                  workingCapsule += '.withWikiName';
-                  workingOptions.wikiName = data.wikiName;
-                }
-
-                return language.$(workingCapsule, workingOptions);
-              })),
+              relations.titleText),
 
             html.tag('meta', {charset: 'utf-8'}),
             html.tag('meta', {
diff --git a/src/content/dependencies/generatePageTitleText.js b/src/content/dependencies/generatePageTitleText.js
new file mode 100644
index 00000000..5482ca91
--- /dev/null
+++ b/src/content/dependencies/generatePageTitleText.js
@@ -0,0 +1,67 @@
+import striptags from 'striptags';
+
+export default {
+  sprawl: ({wikiInfo}) => ({wikiInfo}),
+
+  data: (sprawl) => ({
+    wikiName:
+      sprawl.wikiInfo.nameShort,
+  }),
+
+  slots: {
+    title: {
+      type: 'html',
+      mutable: false,
+    },
+
+    showWikiNameInTitle: {
+      validate: v => v.is(true, false, 'auto'),
+      default: 'auto',
+    },
+
+    subtitle: {
+      type: 'html',
+      mutable: false,
+    },
+  },
+
+  generate: (data, slots, {html, language}) =>
+    language.encapsulate('misc.pageTitle', workingCapsule => {
+      const workingOptions = {};
+
+      // Slightly jank: The output of striptags is, of course, a string,
+      // and as far as language.formatString() is concerned, that means
+      // it needs to be sanitized - including turning ampersands into
+      // &amp;'s. But the title is already HTML that has implicitly been
+      // sanitized, however it got here, and includes HTML entities that
+      // are properly escaped. Those need to get included as they are,
+      // so we wrap the title in a tag and pass it off as good to go.
+      workingOptions.title =
+        html.tags([
+striptags(slots.title.toString()),
+        ]);
+
+      if (!html.isBlank(slots.subtitle)) {
+        // Same shenanigans here, as far as wrapping striptags goes.
+        workingCapsule += '.withSubtitle';
+        workingOptions.subtitle =
+          html.tags([
+            striptags(slots.subtitle.toString()),
+          ]);
+      }
+
+      const showWikiName =
+        (slots.showWikiNameInTitle === true
+? true
+       : slots.showWikiNameInTitle === 'auto'
+? html.isBlank(slots.subtitle)
+: false);
+
+      if (showWikiName) {
+        workingCapsule += '.withWikiName';
+        workingOptions.wikiName = data.wikiName;
+      }
+
+      return language.$(workingCapsule, workingOptions);
+    }),
+};
\ No newline at end of file
diff --git a/src/content/dependencies/generateSearchSidebarBox.js b/src/content/dependencies/generateSearchSidebarBox.js
index 701a01ac..0d760773 100644
--- a/src/content/dependencies/generateSearchSidebarBox.js
+++ b/src/content/dependencies/generateSearchSidebarBox.js
@@ -35,6 +35,11 @@ export default {
           html.tag('template', {class: 'wiki-search-no-results-string'},
             language.$(capsule, 'noResults')),
 
+          html.tag('template', {class: 'wiki-search-back-string'},
+            language.$(capsule, 'back', {
+              page: html.tag('slot', {name: 'page'}),
+            })),
+
           html.tag('template', {class: 'wiki-search-current-result-string'},
             language.$(capsule, 'currentResult')),
 
diff --git a/src/data/things/track.js b/src/data/things/track.js
index 1095cce9..ab7511a8 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -1241,7 +1241,7 @@ export class Track extends Thing {
     tracksWhichAreFollowingProductionsOf: {
       bindTo: 'trackData',
 
-      referencing: track => track,
+      referencing: track => track.isMainRelease ? [track] : [],
       referenced: track => track.previousProductionTracks,
     },
   };
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 4ba766c4..fbb4e5d6 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -44,28 +44,38 @@ function inspect(value, opts = {}) {
   return nodeInspect(value, {colors: ENABLE_COLOR, ...opts});
 }
 
-function pushWikiData(a, b) {
-  for (const key of Object.keys(b)) {
-    if (Object.hasOwn(a, key)) {
-      if (Array.isArray(a[key])) {
-        if (Array.isArray(b[key])) {
-          a[key].push(...b[key]);
-        } else {
-          throw new Error(`${key} already present, expected array of items to push`);
-        }
+function makeEmptyWikiData() {
+  const wikiData = {};
+
+  for (const thingConstructor of Object.values(thingConstructors)) {
+    if (thingConstructor[Thing.wikiData]) {
+      if (thingConstructor[Thing.oneInstancePerWiki]) {
+        wikiData[thingConstructor[Thing.wikiData]] = null;
       } else {
-        if (Array.isArray(a[key])) {
-          throw new Error(`${key} already present and not an array, refusing to overwrite`);
-        } else {
-          throw new Error(`${key} already present, refusing to overwrite`);
-        }
+        wikiData[thingConstructor[Thing.wikiData]] = [];
       }
-    } else {
+    }
+  }
+
+  return wikiData;
+}
+
+function pushWikiData(a, b) {
+  for (const key of Object.keys(b)) {
+    if (!Object.hasOwn(a, key)) {
+      throw new Error(`${key} not present`);
+    }
+
+    if (Array.isArray(a[key])) {
       if (Array.isArray(b[key])) {
-        a[key] = [...b[key]];
+        a[key].push(...b[key]);
       } else {
-        a[key] = b[key];
+        throw new Error(`${key} is an array, expected array of items to push`);
       }
+    } else if (a[key] === null) {
+      a[key] = b[key];
+    } else if (b[key] !== null) {
+      throw new Error(`${key} already has a value: ${inspect(a[key])}`);
     }
   }
 }
@@ -187,7 +197,7 @@ function makeProcessDocument(thingConstructor, {
 
     const thing = Reflect.construct(thingConstructor, []);
 
-    const wikiData = {};
+    const wikiData = makeEmptyWikiData();
     const flat = [thing];
     if (thingConstructor[Thing.wikiData]) {
       if (thingConstructor[Thing.oneInstancePerWiki]) {
@@ -1357,7 +1367,7 @@ export function processThingsFromDataStep(documents, dataStep) {
     case documentModes.allTogether: {
       const things = [];
       const flat = [];
-      const wikiData = {};
+      const wikiData = makeEmptyWikiData();
       const aggregate = openAggregate({message: `Errors processing documents`});
 
       documents.forEach(
@@ -1417,7 +1427,7 @@ export function processThingsFromDataStep(documents, dataStep) {
         throw new Error(`Missing header document (empty file or erroneously starting with "---"?)`);
 
       const aggregate = openAggregate({message: `Errors processing documents`});
-      const wikiData = {};
+      const wikiData = makeEmptyWikiData();
 
       const {result: headerResult, aggregate: headerAggregate} =
         processDocument(headerDocument, dataStep.headerDocumentThing);
@@ -1688,7 +1698,7 @@ export function connectThingsFromDataSteps(processThingResultLists, dataSteps) {
 }
 
 export function makeWikiDataFromDataSteps(processThingResultLists, _dataSteps) {
-  const wikiData = {};
+  const wikiData = makeEmptyWikiData();
 
   for (const result of processThingResultLists.flat(2)) {
     pushWikiData(wikiData, result.wikiData);
diff --git a/src/static/css/site.css b/src/static/css/site.css
index e1654e6d..73b732d8 100644
--- a/src/static/css/site.css
+++ b/src/static/css/site.css
@@ -750,6 +750,12 @@ summary.underline-white > span:hover a:not(:hover) {
   margin: 0;
 }
 
+.wiki-search-context-container {
+  padding: 2px 12px 4px;
+  font-size: 0.9em;
+  border-bottom: 1px solid var(--dim-color);
+}
+
 .wiki-search-results-container {
   margin-bottom: 0;
   padding: 2px;
diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js
index b4356a0f..d63b9708 100644
--- a/src/static/js/client/sidebar-search.js
+++ b/src/static/js/client/sidebar-search.js
@@ -39,6 +39,9 @@ export const info = {
   failedRule: null,
   failedContainer: null,
 
+  contextContainer: null,
+  contextBackLink: null,
+
   filterContainer: null,
   albumFilterLink: null,
   artistFilterLink: null,
@@ -66,6 +69,8 @@ export const info = {
   currentResultString: null,
   endSearchString: null,
 
+  backString: null,
+
   albumResultKindString: null,
   artistResultKindString: null,
   groupResultKindString: null,
@@ -109,6 +114,10 @@ export const info = {
       type: 'string',
     },
 
+    activeQueryContextPageName: {type: 'string'},
+    activeQueryContextPagePathname: {type: 'string'},
+    activeQueryContextPageColor: {type: 'string'},
+
     activeQueryResults: {
       type: 'json',
       maxLength: settings => settings.maxActiveResultsStorage,
@@ -180,6 +189,9 @@ export function getPageReferences() {
   info.noResultsString =
     findString('no-results');
 
+  info.backString =
+    findString('back');
+
   info.currentResultString =
     findString('current-result');
 
@@ -313,6 +325,25 @@ export function mutatePageContent() {
   info.searchBox.appendChild(info.failedRule);
   info.searchBox.appendChild(info.failedContainer);
 
+  // Context section
+
+  info.contextContainer =
+    document.createElement('div');
+
+  info.contextContainer.classList.add('wiki-search-context-container');
+
+  info.contextBackLink =
+    document.createElement('a');
+
+  info.contextContainer.appendChild(
+    templateContent(info.backString, {
+      page: info.contextBackLink,
+    }));
+
+  cssProp(info.contextContainer, 'display', 'none');
+
+  info.searchBox.appendChild(info.contextContainer);
+
   // Filter section
 
   info.filterContainer =
@@ -652,6 +683,17 @@ async function activateSidebarSearch(query) {
   state.searchStage = 'complete';
   updateSidebarSearchStatus();
 
+  session.activeQueryContextPageName =
+    document.querySelector('title').dataset.withoutWikiName ??
+    document.title;
+
+  session.activeQueryContextPagePathname =
+    location.pathname;
+
+  session.activeQueryContextPageColor =
+    document.querySelector('.color-style')?.dataset.color ??
+    null;
+
   session.activeQuery = query;
   session.activeQueryResults = results;
   session.resultsScrollOffset = 0;
@@ -804,6 +846,8 @@ function showSidebarSearchResults(results) {
   }
 
   if (shownAnyResults) {
+    showContextControls();
+
     cssProp(info.endSearchRule, 'display', 'block');
     cssProp(info.endSearchLine, 'display', 'block');
 
@@ -901,6 +945,35 @@ function showFilterElements(results) {
   }
 }
 
+function showContextControls() {
+  const {session} = info;
+
+  const shouldShow =
+    session.activeQueryContextPagePathname &&
+    location.pathname !== session.activeQueryContextPagePathname;
+
+  if (shouldShow) {
+    info.contextBackLink.href =
+      session.activeQueryContextPagePathname;
+
+    cssProp(info.contextBackLink,
+      '--primary-color',
+      session.activeQueryContextPageColor);
+
+    while (info.contextBackLink.firstChild) {
+      info.contextBackLink.firstChild.remove();
+    }
+
+    info.contextBackLink.appendChild(
+      document.createTextNode(
+        session.activeQueryContextPageName));
+
+    cssProp(info.contextContainer, 'display', 'block');
+  } else {
+    cssProp(info.contextContainer, 'display', 'none');
+  }
+}
+
 function generateSidebarSearchResult(result, results) {
   const preparedSlots = {
     color:
@@ -1126,6 +1199,7 @@ function generateSidebarSearchResultTemplate(slots) {
 }
 
 function hideSidebarSearchResults() {
+  cssProp(info.contextContainer, 'display', 'none');
   cssProp(info.filterContainer, 'display', 'none');
 
   cssProp(info.resultsRule, 'display', 'none');
diff --git a/src/strings-default.yaml b/src/strings-default.yaml
index 233a7ed3..dca17bbd 100644
--- a/src/strings-default.yaml
+++ b/src/strings-default.yaml
@@ -834,6 +834,8 @@ misc:
       No results for this query, sorry!
       Check spelling and use complete words.
 
+    back: "Return to: {PAGE}"
+
     currentResult: "(you are here)"
     endSearch: "(OK, I'm done searching now.)"