« 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/common-util/search-shape.js58
-rw-r--r--src/content/dependencies/generatePageLayout.js16
-rw-r--r--src/data/composite/things/track/withDirectorySuffix.js22
-rw-r--r--src/data/composite/things/track/withSuffixDirectoryFromAlbum.js20
-rw-r--r--src/data/things/album.js33
-rw-r--r--src/search-select.js (renamed from src/common-util/search-spec.js)113
-rw-r--r--src/search.js61
-rw-r--r--src/static/js/search-worker.js3
8 files changed, 196 insertions, 130 deletions
diff --git a/src/common-util/search-shape.js b/src/common-util/search-shape.js
new file mode 100644
index 00000000..e0819ed6
--- /dev/null
+++ b/src/common-util/search-shape.js
@@ -0,0 +1,58 @@
+// Index structures shared by client and server, and relevant interfaces.
+// First and foremost, this is complemented by src/search-select.js, which
+// actually fills the search indexes up with stuff. During build this all
+// gets consumed by src/search.js to make an index, fill it with stuff
+// (as described by search-select.js), and export it to disk; then on
+// the client that export is consumed by src/static/js/search-worker.js,
+// which builds an index in the same shape and imports the data for query.
+
+const baselineStore = [
+  'primaryName',
+  'disambiguator',
+  'artwork',
+  'color',
+];
+
+const genericStore = baselineStore;
+
+const searchShape = {
+  generic: {
+    index: [
+      'primaryName',
+      'parentName',
+      'artTags',
+      'additionalNames',
+      'contributors',
+      'groups',
+    ].map(field => ({field, tokenize: 'forward'})),
+
+    store: genericStore,
+  },
+
+  verbatim: {
+    index: [
+      'primaryName',
+      'parentName',
+      'artTags',
+      'additionalNames',
+      'contributors',
+      'groups',
+    ],
+
+    store: genericStore,
+  },
+};
+
+export default searchShape;
+
+export function makeSearchIndex(descriptor, {FlexSearch}) {
+  return new FlexSearch.Document({
+    id: 'reference',
+    index: descriptor.index,
+    store: descriptor.store,
+
+    // Disable scoring, always return results according to provided order
+    // (specified above in `genericQuery`, etc).
+    resolution: 1,
+  });
+}
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index f3fad2db..23d5932d 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -656,13 +656,25 @@ export default {
               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
+                // &'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 =
-                  striptags(slots.title.toString());
+                  html.tags([
+                    striptags(slots.title.toString()),
+                  ]);
 
                 if (!html.isBlank(slots.subtitle)) {
+                  // Same shenanigans here, as far as wrapping striptags goes.
                   workingCapsule += '.withSubtitle';
                   workingOptions.subtitle =
-                    striptags(slots.subtitle.toString());
+                    html.tags([
+                      striptags(slots.subtitle.toString()),
+                    ]);
                 }
 
                 const showWikiName =
diff --git a/src/data/composite/things/track/withDirectorySuffix.js b/src/data/composite/things/track/withDirectorySuffix.js
index c063e158..c3651491 100644
--- a/src/data/composite/things/track/withDirectorySuffix.js
+++ b/src/data/composite/things/track/withDirectorySuffix.js
@@ -1,8 +1,9 @@
 import {input, templateCompositeFrom} from '#composite';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
 
-import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+import withContainingTrackSection from './withContainingTrackSection.js';
 import withSuffixDirectoryFromAlbum from './withSuffixDirectoryFromAlbum.js';
 
 export default templateCompositeFrom({
@@ -16,21 +17,16 @@ export default templateCompositeFrom({
     raiseOutputWithoutDependency({
       dependency: '#suffixDirectoryFromAlbum',
       mode: input.value('falsy'),
-      output: input.value({['#directorySuffix']: null}),
+      output: input.value({'#directorySuffix': null}),
     }),
 
-    withPropertyFromAlbum({
+    withContainingTrackSection(),
+
+    withPropertyFromObject({
+      object: '#trackSection',
       property: input.value('directorySuffix'),
+    }).outputs({
+      '#trackSection.directorySuffix': '#directorySuffix',
     }),
-
-    {
-      dependencies: ['#album.directorySuffix'],
-      compute: (continuation, {
-        ['#album.directorySuffix']: directorySuffix,
-      }) => continuation({
-        ['#directorySuffix']:
-          directorySuffix,
-      }),
-    },
   ],
 });
diff --git a/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
index 7159a3f4..30c777b6 100644
--- a/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
+++ b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
@@ -1,8 +1,9 @@
 import {input, templateCompositeFrom} from '#composite';
 
 import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
 
-import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+import withContainingTrackSection from './withContainingTrackSection.js';
 
 export default templateCompositeFrom({
   annotation: `withSuffixDirectoryFromAlbum`,
@@ -36,18 +37,13 @@ export default templateCompositeFrom({
           : continuation()),
     },
 
-    withPropertyFromAlbum({
+    withContainingTrackSection(),
+
+    withPropertyFromObject({
+      object: '#trackSection',
       property: input.value('suffixTrackDirectories'),
+    }).outputs({
+      '#trackSection.suffixTrackDirectories': '#suffixDirectoryFromAlbum',
     }),
-
-    {
-      dependencies: ['#album.suffixTrackDirectories'],
-      compute: (continuation, {
-        ['#album.suffixTrackDirectories']: suffixTrackDirectories,
-      }) => continuation({
-        ['#suffixDirectoryFromAlbum']:
-          suffixTrackDirectories,
-      }),
-    },
   ],
 });
diff --git a/src/data/things/album.js b/src/data/things/album.js
index 0f018e44..58d5253c 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -1049,6 +1049,36 @@ export class TrackSection extends Thing {
 
     unqualifiedDirectory: directory(),
 
+    directorySuffix: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isDirectory),
+      }),
+
+      withAlbum(),
+
+      withPropertyFromObject({
+        object: '#album',
+        property: input.value('directorySuffix'),
+      }),
+
+      exposeDependency({dependency: '#album.directorySuffix'}),
+    ],
+
+    suffixTrackDirectories: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isBoolean),
+      }),
+
+      withAlbum(),
+
+      withPropertyFromObject({
+        object: '#album',
+        property: input.value('suffixTrackDirectories'),
+      }),
+
+      exposeDependency({dependency: '#album.suffixTrackDirectories'}),
+    ],
+
     color: [
       exposeUpdateValueOrContinue({
         validate: input.value(isColor),
@@ -1175,6 +1205,9 @@ export class TrackSection extends Thing {
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Section': {property: 'name'},
+      'Directory Suffix': {property: 'directorySuffix'},
+      'Suffix Track Directories': {property: 'suffixTrackDirectories'},
+
       'Color': {property: 'color'},
       'Start Counting From': {property: 'startCountingFrom'},
 
diff --git a/src/common-util/search-spec.js b/src/search-select.js
index 731e5495..68d2f4e9 100644
--- a/src/common-util/search-spec.js
+++ b/src/search-select.js
@@ -1,4 +1,9 @@
-// Index structures shared by client and server, and relevant interfaces.
+// Complements the specs in search-shape.js with the functions that actually
+// process live wiki data into records that are appropriate for storage.
+// These files totally go together, so read them side by side, okay?
+
+import baseSearchSpec from '#search-shape';
+import {getKebabCase} from '#wiki-data';
 
 function prepareArtwork(artwork, thing, {
   checkIfImagePathHasCachedThumbnails,
@@ -65,14 +70,7 @@ function baselineProcess(thing, opts) {
   return fields;
 }
 
-const baselineStore = [
-  'primaryName',
-  'disambiguator',
-  'artwork',
-  'color',
-];
-
-function genericQuery(wikiData) {
+function genericSelect(wikiData) {
   const groupOrder =
     wikiData.wikiInfo.divideTrackListsByGroups;
 
@@ -108,7 +106,10 @@ function genericQuery(wikiData) {
 
     sortByGroupRank(
       wikiData.trackData
-        .filter(track => !track.mainReleaseTrack)),
+        .filter(track =>
+          track.isMainRelease ||
+          (getKebabCase(track.name) !==
+           getKebabCase(track.mainReleaseTrack.name)))),
   ].flat();
 }
 
@@ -197,96 +198,20 @@ function genericProcess(thing, opts) {
   return fields;
 }
 
-const genericStore = baselineStore;
-
-export const searchSpec = {
+const spiffySearchSpec = {
   generic: {
-    query: genericQuery,
-    process: genericProcess,
+    ...baseSearchSpec.generic,
 
-    index: [
-      'primaryName',
-      'parentName',
-      'artTags',
-      'additionalNames',
-      'contributors',
-      'groups',
-    ].map(field => ({field, tokenize: 'forward'})),
-
-    store: genericStore,
+    select: genericSelect,
+    process: genericProcess,
   },
 
   verbatim: {
-    query: genericQuery,
-    process: genericProcess,
+    ...baseSearchSpec.verbatim,
 
-    index: [
-      'primaryName',
-      'parentName',
-      'artTags',
-      'additionalNames',
-      'contributors',
-      'groups',
-    ],
-
-    store: genericStore,
+    select: genericSelect,
+    process: genericProcess,
   },
 };
 
-export function makeSearchIndex(descriptor, {FlexSearch}) {
-  return new FlexSearch.Document({
-    id: 'reference',
-    index: descriptor.index,
-    store: descriptor.store,
-
-    // Disable scoring, always return results according to provided order
-    // (specified above in `genericQuery`, etc).
-    resolution: 1,
-  });
-}
-
-// TODO: This function basically mirrors bind-utilities.js, which isn't
-// exactly robust, but... binding might need some more thought across the
-// codebase in *general.*
-function bindSearchUtilities({
-  checkIfImagePathHasCachedThumbnails,
-  getThumbnailEqualOrSmaller,
-  thumbsCache,
-  urls,
-}) {
-  const bound = {
-    urls,
-  };
-
-  bound.checkIfImagePathHasCachedThumbnails =
-    (imagePath) =>
-      checkIfImagePathHasCachedThumbnails(imagePath, thumbsCache);
-
-  bound.getThumbnailEqualOrSmaller =
-    (preferred, imagePath) =>
-      getThumbnailEqualOrSmaller(preferred, imagePath, thumbsCache);
-
-  return bound;
-}
-
-export function populateSearchIndex(index, descriptor, opts) {
-  const {wikiData} = opts;
-  const bound = bindSearchUtilities(opts);
-
-  const collection = descriptor.query(wikiData);
-
-  for (const thing of collection) {
-    const reference = thing.constructor.getReference(thing);
-
-    let processed;
-    try {
-      processed = descriptor.process(thing, bound);
-    } catch (caughtError) {
-      throw new Error(
-        `Failed to process searchable thing ${reference}`,
-        {cause: caughtError});
-    }
-
-    index.add({reference, ...processed});
-  }
-}
+export default spiffySearchSpec;
diff --git a/src/search.js b/src/search.js
index a2dae9e1..138a2d2c 100644
--- a/src/search.js
+++ b/src/search.js
@@ -9,11 +9,53 @@ import FlexSearch from 'flexsearch';
 import {pack} from 'msgpackr';
 
 import {logWarn} from '#cli';
-import {makeSearchIndex, populateSearchIndex, searchSpec} from '#search-spec';
+import {makeSearchIndex} from '#search-shape';
+import searchSpec from '#search-select';
 import {stitchArrays} from '#sugar';
 import {checkIfImagePathHasCachedThumbnails, getThumbnailEqualOrSmaller}
   from '#thumbs';
 
+// TODO: This function basically mirrors bind-utilities.js, which isn't
+// exactly robust, but... binding might need some more thought across the
+// codebase in *general.*
+function bindSearchUtilities({
+  checkIfImagePathHasCachedThumbnails,
+  getThumbnailEqualOrSmaller,
+  thumbsCache,
+  urls,
+}) {
+  const bound = {
+    urls,
+  };
+
+  bound.checkIfImagePathHasCachedThumbnails =
+    (imagePath) =>
+      checkIfImagePathHasCachedThumbnails(imagePath, thumbsCache);
+
+  bound.getThumbnailEqualOrSmaller =
+    (preferred, imagePath) =>
+      getThumbnailEqualOrSmaller(preferred, imagePath, thumbsCache);
+
+  return bound;
+}
+
+function populateSearchIndex(index, descriptor, wikiData, utilities) {
+  for (const thing of descriptor.select(wikiData)) {
+    const reference = thing.constructor.getReference(thing);
+
+    let processed;
+    try {
+      processed = descriptor.process(thing, utilities);
+    } catch (caughtError) {
+      throw new Error(
+        `Failed to process searchable thing ${reference}`,
+        {cause: caughtError});
+    }
+
+    index.add({reference, ...processed});
+  }
+}
+
 async function serializeIndex(index) {
   const results = {};
 
@@ -60,17 +102,20 @@ export async function writeSearchData({
       .map(descriptor =>
         makeSearchIndex(descriptor, {FlexSearch}));
 
+  const utilities =
+    bindSearchUtilities({
+      checkIfImagePathHasCachedThumbnails,
+      getThumbnailEqualOrSmaller,
+      thumbsCache,
+      urls,
+      wikiData,
+    });
+
   stitchArrays({
     index: indexes,
     descriptor: descriptors,
   }).forEach(({index, descriptor}) =>
-      populateSearchIndex(index, descriptor, {
-        checkIfImagePathHasCachedThumbnails,
-        getThumbnailEqualOrSmaller,
-        thumbsCache,
-        urls,
-        wikiData,
-      }));
+      populateSearchIndex(index, descriptor, wikiData, utilities));
 
   const serializedIndexes =
     await Promise.all(indexes.map(serializeIndex));
diff --git a/src/static/js/search-worker.js b/src/static/js/search-worker.js
index 3e9fbfca..387cbca0 100644
--- a/src/static/js/search-worker.js
+++ b/src/static/js/search-worker.js
@@ -2,7 +2,8 @@
 
 import FlexSearch from '../lib/flexsearch/flexsearch.bundle.module.min.js';
 
-import {makeSearchIndex, searchSpec} from '../shared-util/search-spec.js';
+import {default as searchSpec, makeSearchIndex}
+  from '../shared-util/search-shape.js';
 
 import {
   empty,